GitOps – Liferay Deployment

GitOps

In diesem Artikel zeige ich wie man mit GitOps (FluxCD) einen Liferay Cluster in Betrieb nehmen kann. Voraussetzung ist das ein Kubernetes Cluster (z.B. mit K3D) mit FluxCD bereits läuft und ein passendes Git Repository mit den Deploy Keys zur Verfügung steht.

Es wird davon ausgegangen das die Verzeichnisse workloads und namespaces von FluxCD überwacht werden und FluxCD entsprechend konfiguriert ist.

Ferner muss die allgemeine Arbeitsweise von GitOps bekannt sein, d.h. es wird davon ausgegangen das sie wissen wie man zum Beispiel die Synchronisation manuell mit fluxctl auslösen kann, um die Arbeitsgeschwindigkeit zu erhöhen.

Ziel ist es einen kleinen Cluster mit 2 bis maximal 4 Instanzen von Liferay CE zu erzeugen.

Liferay Komponenten

Um Liferay CE bereitzustellen, benötigen wir einige Komponenten die wir nun der Reihe nach deployen werden. Wir werden zunächst einen Namespace erstellen und dann die Environment für Liferay erzeugen. Diese besteht aus Elasticsearch und der freien Datenbank MySql in der Version 8.0. Zusätzlich müssen wir für die Konfiguration von Elasticsearch und Liferay ConfigMaps erzeugen, damit alles zusammen funktioniert.

Namespace

Wir wollen simulieren, dass das Liferay in einer Prod Umgebung laufen soll und daher erstellen wir zunächst einen passenden Namespace liferay-prod.

Erstellen sie in dem Git Repository in dem Verzeichnis namespaces folgendes liferay-prod.yaml

apiVersion: v1
kind: Namespace
metadata:
  labels:
    name: liferay-prod
  name: liferay-prod

Nach der Synchronisation durch FluxCD wird in dem Kubernetes Cluster ein neuer Namespace liferay-prod angelegt.

MySql

Für die Datenbank verwende ich ein MySql in der Version 8, da diese in der Compatibility Matrix gelistet wird.

Für die Persistenz wird ein PVC in der Größenordnung von 100MB vorgehalten.

apiVersion: v1
kind: Service
metadata:
  name: database
  namespace: liferay-prod
spec:
  ports:
  - port: 3306
  selector:
    app: database


---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app: database-data
  name: database-data
  namespace: liferay-prod
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi


---


apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: database
  name: database
  namespace: liferay-prod
spec:
  replicas: 1
  selector:
    matchLabels:
      app: database
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: database
    spec:
      containers:
      - args:
        - mysqld
        - --character-set-server=utf8
        - --collation-server=utf8_general_ci
        - --character-set-filesystem=utf8
        env:
        - name: MYSQL_DATABASE
          value: lportal
        - name: MYSQL_PASSWORD
          value: roto2
        - name: MYSQL_ROOT_PASSWORD
          value: root
        - name: MYSQL_USER
          value: roto2
        image: mysql:8.0
        resources:
          limits:
            memory: "512Mi"
            cpu: "1"
        ports:
        - containerPort: 3306
        name: database
        volumeMounts:
        - mountPath: /var/lib/mysql
          name: database-data
      restartPolicy: Always
      volumes:
      - name: database-data
        persistentVolumeClaim:
          claimName: database-data

Elasticsearch

apiVersion: v1
kind: Service
metadata:
  name: search
  namespace: liferay-prod
spec:
  ports:
  - port: 9300
  selector:
    app: search

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app: search-claim
  name: search-claim
  namespace: liferay-prod
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi

---

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: search
  name: search
  namespace: liferay-prod
spec:
  replicas: 1
  selector:
    matchLabels:
      app: search
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: search
    spec:
      containers:
      - env:
        - name: ES_JAVA_OPTS
          value: -Xms1g -Xmx1g
        - name: LCP_PROJECT_ENVIRONMENT
          value: local
        - name: LCP_SERVICE_ID
          value: search
        - name: LCP_SERVICE_SCALE
          value: "1"
        - name: cluster.routing.allocation.disk.threshold_enabled
          value: "false"
        - name: discovery.type
          value: single-node
        image: liferaycloud/elasticsearch:6.8.4-3.0.5
        resources:
          limits:
            memory: "2024Mi"
            cpu: "2"
        ports:
        - containerPort: 9300
        name: search
        volumeMounts:
        - mountPath: /lcp-container
          name: search-claim
      restartPolicy: Always
      volumes:
      - name: search-claim
        persistentVolumeClaim:
          claimName: search-claim

Elasticsearch ConfigMap

Erstellen sie das Kubernetes Manifest com.liferay.portal.search.elasticsearch6.configuration.ElasticsearchConfiguration.cfg in dem workloads Ordner mit folgenden Inhalt:

apiVersion: v1
kind: ConfigMap
metadata:
  name: elasticsearchconfiguration.cfg
  namespace: liferay-prod
data:
  com.liferay.portal.search.elasticsearch6.configuration.ElasticsearchConfiguration.cfg: |
    operationMode="REMOTE"
    indexNamePrefix="liferay-"
    transportAddresses="search.liferay-prod.svc.cluster.local:9300"
    clusterName="liferay_cluster"
    logExceptionsOnly="false"

Wie das Inlining von Dateien in YAML funktioniert, dazu mehr in einem weiteren Artikel YAML Multiline Collections.

Die Datei wird später in dem Liferay Deployment in den Pfad /mnt/liferay/files/com.liferay.portal.search.elasticsearch6.configuration.ElasticsearchConfiguration.cfg gemounted. Von hier kopiert sich der Liferay Container die Konfiguration von Elasticsearch und wendet sie beim Starten des Containers an. So ist eine Konfiguration von außen möglich.

Liferay

Die liveness und readyness Proben sind hier mit Absicht etwas höher angesetzt, da ich davon ausgehe dass es zum Testen in einer VM gestartet wird. Das heißt aber auch das eine weitere Instanz erst nach 2 Minuten frühestens bereit steht.

Es werden sofort 2 Instanzen gestartet und maximal auf 4 in HPA (Kubernetes Horizontal Pod Autoscaler) erhöht. Das heißt man sollte hier für dieses Beispiel genügend Arbeitspeicher frei haben.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: liferay
  name: liferay--cluster
  namespace: liferay-prod
  annotations:
    traefik.ingress.kubernetes.io/affinity: "true"
    traefik.ingress.kubernetes.io/session-cookie-name: "LIFERAY-STICKY"
spec:
  ports:
  - name: "8080"
    port: 8080
  selector:
    app: liferay

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app: liferay-data
  name: liferay-data
  namespace: liferay-prod
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi


---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app: liferay-config
  name: liferay-config
  namespace: liferay-prod
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi

---

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: liferay
  name: liferay
  namespace: liferay-prod
spec:
  selector:
    matchLabels:
      app: liferay
  strategy:
    type: RollingUpdate # Updatestrategie auf RollingUpdate setzen
  template:
    metadata:
      labels:
        app: liferay
    spec:
      containers:
      - env: 
        - name: LIFERAY_JAVA_OPTS 
          value: -Xms2g -Xmx2g
        - name: LIFERAY_MODULE_PERIOD_FRAMEWORK_PERIOD_PROPERTIES_PERIOD_OSGI_PERIOD_CONSOLE
          value: 0.0.0.0:11311
        - name: LIFERAY_WEB_PERIOD_SERVER_PERIOD_DISPLAY_PERIOD_NODE # Anzeige im Footer auf welcher NODE die Seite gerendert worden ist
          value: "true"
        - name: LIFERAY_REDIRECT_PERIOD_URL_PERIOD_SECURITY_PERIOD_MODE #La usaremos para permitir los redirect en el cluster
          value: "domain"
        image: liferay/portal:7.3.2-ga3
        resources:
          requests: #
            memory: "2048Mi"
            cpu: "2"
          limits: #
            memory: "4098Mi"
            cpu: "3"
        name: liferay 
        ports: # Port 11311 GoGo-Shell
        - containerPort: 11311
        - containerPort: 8080
        readinessProbe: # frühestens die Readyness Probe nach 120 Sekunden durchführen auf http:// c/portal/layout
          httpGet:
            path: "/c/portal/layout"
            port: 8080
          initialDelaySeconds: 120
          periodSeconds: 15
          failureThreshold: 3
          successThreshold: 3
        livenessProbe: # liveness Proben auch erst nach 120 Sekunden durchführen
          tcpSocket:
            port: 8080
          initialDelaySeconds: 120
          periodSeconds: 20
          failureThreshold: 3
          successThreshold: 1
        volumeMounts: # daten und liferay Verzeichniss mounten
        - mountPath: /opt/liferay/data
          name: liferay-data
        - mountPath: /mnt/liferay
          name: liferay-config
        - mountPath: /mnt/liferay/files/portal-ext.properties
          subPath: portal-ext.properties
          name: config-portal-ext-properties
        - mountPath: /mnt/liferay/files/osgi/configs/com.liferay.portal.search.elasticsearch6.configuration.ElasticsearchConfiguration.cfg
          subPath: com.liferay.portal.search.elasticsearch6.configuration.ElasticsearchConfiguration.cfg
          name: config-elasticsearch
      restartPolicy: Always #politica de reinicio ante errores en el contenedor
      volumes: # PVC mappen
      - name: liferay-data
        persistentVolumeClaim:
          claimName: liferay-data
      - name: liferay-config
        persistentVolumeClaim:
          claimName: liferay-config
      - name: config-portal-ext-properties
        configMap:
          name: portal-ext.properties
      - name: config-elasticsearch
        configMap:
          name: elasticsearchconfiguration.cfg
---

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: liferay
  namespace: liferay-prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: liferay
  behavior:
        scaleUp:
            stabilizationWindowSeconds: 300
        scaleDown:
            selectPolicy: Disabled
  minReplicas: 2
  maxReplicas: 4

Portal-ext.properties

Die Konfigurationsdatei portal-ext.properties muss nach /mnt/liferay/files/portal-ext.properties gemappt werden, damit der Liferay Container sie beim Start kopieren und letztlich einbinden kann.

apiVersion: v1
kind: ConfigMap
metadata:
  name: portal-ext.properties
  namespace: liferay-prod
data:
  portal-ext.properties: |
    # This is main Liferay configuration file, common (shared) for all Liferay environments.
    #
    # Liferay Workspace will copy this file into Liferay bundle's root directory (= ${liferay.home})
    # when Liferay bundle is being built.

    ##
    ## JDBC
    ##


    jdbc.default.driverClassName=com.mysql.cj.jdbc.Driver
    jdbc.default.url=jdbc:mysql://database:3306/lportal?dontTrackOpenResources=true&holdResultsOpenOverStatementClose=true&useFastDateParsing=false
    jdbc.default.username=roto2
    jdbc.default.password=roto2

    ##
    ## Retry JDBC connection on portal startup.
    ##

    #
    # Set the number of seconds to retry getting a JDBC connection on portal
    # startup.
    #
    retry.jdbc.on.startup.delay=5

    #
    # Set the max number of times to retry getting a JDBC connection on portal
    # startup.
    #
    retry.jdbc.on.startup.max.retries=5

    ##
    ## Company
    ##

    company.default.name=Liferay Kubernetes

    #
    # This sets the default web ID. Omniadmin users must belong to the company
    # with this web ID.
    #
    company.default.web.id=liferay.com

    ##
    ## Servlet Filters
    ##

    #
    # If the user can unzip compressed HTTP content, the GZip filter will
    # zip up the HTTP content before sending it to the user. This will speed up
    # page rendering for users that are on dial up.
    #
    com.liferay.portal.servlet.filters.gzip.GZipFilter=false

    #
    # The NTLM filter is used to provide NTLM based single sign on.
    #
    com.liferay.portal.servlet.filters.sso.ntlm.NtlmFilter=false

    #
    # The NTLM post filter is used to fix known issues with NTLM and ajax
    # requests. See LPS-3795.
    #
    com.liferay.portal.servlet.filters.sso.ntlm.NtlmPostFilter=false



    ##
    #   # Cluster Link
    #   #

    #
    # Set this to true to enable the cluster link. This is required if you want
    # to cluster indexing and other features that depend on the cluster link.
    #
    cluster.link.enabled=true

    ehcache.cluster.link.replication.enabled=true

    #
    # Set this property to autodetect the default outgoing IP address so that
    # JGroups can bind to it. The property must point to an address that is
    # accessible to the portal server, www.google.com, or your local gateway.
    #
    cluster.link.autodetect.address=database:3306

Ingress

Kubernetes nutzt für das Routing in dem Cluster einen sogenannten Ingress Controller, dieser muss wissen wenn eine HTTP Anfrage kommt an wen er diese weiterreichen muss.

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: nginx-ingress
  namespace: liferay-prod
  annotations:
    traefik.ingress.kubernetes.io/affinity: "true"
spec:
  rules:
  - host: liferay.kubernetes.com
    http:
      paths:
      - backend:
          serviceName: liferay--cluster
          servicePort: 8080
        path: /

Session Coockie

Damit Liferay in mehren Instanzen in dem Cluster laufen kann, muss sichergestellt sein, dass die Anfragen auf dem Server wieder landen, wo der Benutzer angemeldet ist.

K3S verwendet nicht den Standard Ingress Controller, sondern Treafik als Ingress Controller. Daher weicht hier die Konfiguration für das Sticky Session Cookie etwas ab.

Affinity auf true setzen

In dem Ingress Manifest muss affinity auf true gesetzt werden (s.o.). Mit kubectl describe kann das getestet werden.

k describe ing nginx-ingress -n liferay-prod
Name:             nginx-ingress
Namespace:        liferay-prod
Address:          172.25.0.4
Default backend:  default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
  Host                    Path  Backends
  ----                    ----  --------
  liferay.kubernetes.com  
                          /   liferay--cluster:8080 (10.42.3.10:8080,10.42.3.11:8080,10.42.3.8:8080 + 1 more...)
Annotations:              fluxcd.io/sync-checksum: e8c9246233bf4aadd61a46a41e2ca106d4ff6eb6
                          traefik.ingress.kubernetes.io/affinity: true
Events:                   <none>

Die Meldung das kein default Controller gesetzt ist, kann ignoriert werden.

Service

Die Konfiguration von Treafik erfolgt über Annotationen im Service. Mehr dazu in der Konfiguration Traefik.

annotations:
    traefik.ingress.kubernetes.io/affinity: "true"
    traefik.ingress.kubernetes.io/session-cookie-name: "LIFERAY-STICKY"

Cookies überprüfen

Zum Testen prüfen wir mit curl ob das Cookie gesetzt wird.

curl -I http://liferay.kubernetes.com | grep LIFERAY-STICKY