diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/00-namespace.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/00-namespace.yaml new file mode 100644 index 0000000..0b87267 --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/00-namespace.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes + app.kubernetes.io/name: ghost-on-kubernetes + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: namespace + app.kubernetes.io/part-of: ghost-on-kubernetes diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/01-ghost-config-externalsecret.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/01-ghost-config-externalsecret.yaml new file mode 100644 index 0000000..0020f94 --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/01-ghost-config-externalsecret.yaml @@ -0,0 +1,17 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: ghost-config + namespace: ghost-on-kubernetes +spec: + refreshInterval: 1h + secretStoreRef: + name: vault-backend + kind: ClusterSecretStore + target: + name: ghost-config + data: + - secretKey: gmail-app-password + remoteRef: + key: k8s_home/ghost # Vault path (without 'data/' prefix) + property: gmail-app-password diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/01-mysql-config-externalsecret.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/01-mysql-config-externalsecret.yaml new file mode 100644 index 0000000..2d9822a --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/01-mysql-config-externalsecret.yaml @@ -0,0 +1,42 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: ghost-on-kubernetes-mysql-env + namespace: ghost-on-kubernetes +spec: + refreshInterval: 1h + secretStoreRef: + name: vault-backend + kind: ClusterSecretStore + target: + name: ghost-on-kubernetes-mysql-env # resulting K8s secret name + data: + - secretKey: MYSQL_DATABASE # key in K8s secret + remoteRef: + key: k8s_home/ghost # Vault path (without 'data/' prefix) + property: mysql-db-name # field within Vault secret + - secretKey: MYSQL_USER # key in K8s secret + remoteRef: + key: k8s_home/ghost + property: mysql-db-user + - secretKey: MYSQL_PASSWORD + remoteRef: + key: k8s_home/ghost + property: mysql-db-password + - secretKey: MYSQL_ROOT_PASSWORD + remoteRef: + key: k8s_home/ghost + property: mysql-db-root-password + - secretKey: MYSQL_HOST + remoteRef: + key: k8s_home/ghost + property: mysql-host + + +# type: Opaque +# stringData: +# MYSQL_DATABASE: mysql-db-name # Same as in config.production.json +# MYSQL_USER: mysql-db-user # Same as in config.production.json +# MYSQL_PASSWORD: mysql-db-password # Same as in config.production.json +# MYSQL_ROOT_PASSWORD: mysql-db-root-password # Same as in config.production.json +# MYSQL_HOST: '%' # Same as in config.production.json diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/01-mysql-config.yaml-not-used-anymore b/gitops/home-kubernetes/ghost-on-kubernetes/01-mysql-config.yaml-not-used-anymore new file mode 100644 index 0000000..b9d2a41 --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/01-mysql-config.yaml-not-used-anymore @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ghost-on-kubernetes-mysql-env + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes-mysql + app.kubernetes.io/name: ghost-on-kubernetes-mysql-env + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: database-secret + app.kubernetes.io/part-of: ghost-on-kubernetes + +type: Opaque +stringData: + MYSQL_DATABASE: mysql-db-name # Same as in config.production.json + MYSQL_USER: mysql-db-user # Same as in config.production.json + MYSQL_PASSWORD: mysql-db-password # Same as in config.production.json + MYSQL_ROOT_PASSWORD: mysql-db-root-password # Same as in config.production.json + MYSQL_HOST: '%' # Same as in config.production.json + diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/01-tls.yaml-not-used-anymore b/gitops/home-kubernetes/ghost-on-kubernetes/01-tls.yaml-not-used-anymore new file mode 100644 index 0000000..91af2c5 --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/01-tls.yaml-not-used-anymore @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: tls-secret + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes + app.kubernetes.io/name: tls-secret + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: tls-secret + app.kubernetes.io/part-of: ghost-on-kubernetes + + +type: kubernetes.io/tls +stringData: + tls.crt: content-tls-crt-base64 # Optional, if you want to use your own TLS certificate + tls.key: content-tls-key-base64 # Optional, if you want to use your own TLS certificate diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/02-pvc.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/02-pvc.yaml new file mode 100644 index 0000000..b61ce0a --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/02-pvc.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: k8s-ghost-content + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes + app.kubernetes.io/name: k8s-ghost-content + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: storage + app.kubernetes.io/part-of: ghost-on-kubernetes + +spec: + # Change this to your storageClassName, we suggest using a storageClassName that supports ReadWriteMany for production. + storageClassName: freenas-iscsi + volumeMode: Filesystem + # Change this to your accessModes. We suggest ReadWriteMany for production, ReadWriteOnce for development. + # With ReadWriteMany, you can have multiple replicas of Ghost, so you can achieve high availability. + # Note that ReadWriteMany is not supported by all storage providers and may require additional configuration. + # Ghost officialy doesn't support HA, they suggest using a CDN or caching. Info: https://ghost.org/docs/faq/clustering-sharding-multi-server/ + accessModes: + - ReadWriteOnce # Change this to your accessModes if needed, we suggest ReadWriteMany so we can scale the deployment later. + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ghost-on-kubernetes-mysql-pvc + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes-mysql + app.kubernetes.io/name: ghost-on-kubernetes-mysql-pvc + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: database-storage + app.kubernetes.io/part-of: ghost-on-kubernetes + + +spec: + storageClassName: freenas-iscsi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/03-service.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/03-service.yaml new file mode 100644 index 0000000..018c185 --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/03-service.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: Service +metadata: + name: ghost-on-kubernetes-service + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes + app.kubernetes.io/name: ghost-on-kubernetes-service + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: service-frontend + app.kubernetes.io/part-of: ghost-on-kubernetes + + +spec: + ports: + - port: 2368 + protocol: TCP + targetPort: ghk8s + name: ghk8s + type: ClusterIP + selector: + app: ghost-on-kubernetes + +--- +apiVersion: v1 +kind: Service +metadata: + name: ghost-on-kubernetes-mysql-service + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes-mysql + app.kubernetes.io/name: ghost-on-kubernetes-mysql-service + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: service-database + app.kubernetes.io/part-of: ghost-on-kubernetes + +spec: + ports: + - port: 3306 + protocol: TCP + targetPort: mysqlgh + name: mysqlgh + type: ClusterIP + clusterIP: None + selector: + app: ghost-on-kubernetes-mysql + diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/04-ghost-config.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/04-ghost-config.yaml new file mode 100644 index 0000000..d9fb06b --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/04-ghost-config.yaml @@ -0,0 +1,59 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ghost-config-prod + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes + app.kubernetes.io/name: ghost-config-prod + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: ghost-config + app.kubernetes.io/part-of: ghost-on-kubernetes +type: Opaque +stringData: + config.production.json: |- + { + "url": "https://ghost.lab.home.hrajfrisbee.cz", + "admin": { + "url": "https://ghost.lab.home.hrajfrisbee.cz" + }, + "server": { + "port": 2368, + "host": "0.0.0.0" + }, + "mail": { + "transport": "SMTP", + "from": "user@server.com", + "options": { + "service": "Google", + "host": "smtp.gmail.com", + "port": 465, + "secure": true, + "auth": { + "user": "user@server.com", + "pass": "passsword" + } + } + }, + "logging": { + "transports": [ + "stdout" + ] + }, + "database": { + "client": "mysql", + "connection": + { + "host": "ghost-on-kubernetes-mysql-service", + "user": "mysql-db-user", + "password": "mysql-db-password", + "database": "mysql-db-name", + "port": "3306" + } + }, + "process": "local", + "paths": { + "contentPath": "/home/nonroot/app/ghost/content" + } + } diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/05-mysql.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/05-mysql.yaml new file mode 100644 index 0000000..6727403 --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/05-mysql.yaml @@ -0,0 +1,134 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: ghost-on-kubernetes-mysql + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes-mysql + app.kubernetes.io/name: ghost-on-kubernetes-mysql + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: database + app.kubernetes.io/part-of: ghost-on-kubernetes + +spec: + serviceName: ghost-on-kubernetes-mysql-service + replicas: 1 + selector: + matchLabels: + app: ghost-on-kubernetes-mysql + template: + metadata: + labels: + app: ghost-on-kubernetes-mysql + spec: + initContainers: + - name: ghost-on-kubernetes-mysql-init + securityContext: + allowPrivilegeEscalation: false + privileged: false + readOnlyRootFilesystem: true + image: docker.io/busybox:stable-musl + imagePullPolicy: Always # You can change this value according to your needs + command: + - /bin/sh + - -c + - | + set -e + echo 'Changing ownership of mysql mount directory to 65534:65534' + chown -Rfv 65534:65534 /mnt/mysql || echo 'Error changing ownership of mysql mount directory to 65534:65534' + echo 'Changing ownership of tmp mount directory to 65534:65534' + chown -Rfv 65534:65534 /mnt/tmp || echo 'Error changing ownership of tmp mount directory to 65534:65534' + echo 'Changing ownership of socket mount directory to 65534:65534' + chown -Rfv 65534:65534 /mnt/var/run/mysqld || echo 'Error changing ownership of socket mount directory to 65534:65534' + + + volumeMounts: + - name: ghost-on-kubernetes-mysql-volume + mountPath: /mnt/mysql + subPath: mysql-empty-subdir + readOnly: false + + - name: ghost-on-kubernetes-mysql-tmp + mountPath: /mnt/tmp + readOnly: false + + - name: ghost-on-kubernetes-mysql-socket + mountPath: /mnt/var/run/mysqld + readOnly: false + + # YOu can ajust the resources according to your needs + resources: + requests: + memory: 0Mi + cpu: 0m + limits: + memory: 1Gi + cpu: 900m + + containers: + - name: ghost-on-kubernetes-mysql + securityContext: + allowPrivilegeEscalation: false + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + + image: docker.io/mysql:8.4 + imagePullPolicy: Always # You can change this value according to your needs + envFrom: + - secretRef: + name: ghost-on-kubernetes-mysql-env + resources: + requests: + memory: 500Mi # You can change this value according to your needs + cpu: 300m # You can change this value according to your needs + limits: + memory: 1Gi # You can change this value according to your needs + cpu: 900m # You can change this value according to your needs + ports: + - containerPort: 3306 + protocol: TCP + name: mysqlgh + volumeMounts: + - name: ghost-on-kubernetes-mysql-volume + mountPath: /var/lib/mysql + subPath: mysql-empty-subdir + readOnly: false + + - name: ghost-on-kubernetes-mysql-tmp + mountPath: /tmp + readOnly: false + + - name: ghost-on-kubernetes-mysql-socket + mountPath: /var/run/mysqld + readOnly: false + + automountServiceAccountToken: false + + # Optional: Uncomment the following to specify node selectors + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: node-role.kubernetes.io/worker + # operator: In + # values: + # - 'true' + + securityContext: + seccompProfile: + type: RuntimeDefault + + volumes: + - name: ghost-on-kubernetes-mysql-volume + persistentVolumeClaim: + claimName: ghost-on-kubernetes-mysql-pvc + - name: ghost-on-kubernetes-mysql-tmp + emptyDir: + sizeLimit: 128Mi + - name: ghost-on-kubernetes-mysql-socket + emptyDir: + sizeLimit: 128Mi diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/06-ghost-deployment.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/06-ghost-deployment.yaml new file mode 100644 index 0000000..47230ad --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/06-ghost-deployment.yaml @@ -0,0 +1,214 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ghost-on-kubernetes + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes + app.kubernetes.io/name: ghost-on-kubernetes + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: ghost + app.kubernetes.io/part-of: ghost-on-kubernetes + + +spec: + # If you want HA for your Ghost instance, you can increase the number of replicas AFTER creation and you need to adjust the storage class. See 02-pvc.yaml for more information. + replicas: 1 + selector: + matchLabels: + app: ghost-on-kubernetes + minReadySeconds: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 3 + revisionHistoryLimit: 4 + progressDeadlineSeconds: 600 + template: + metadata: + namespace: ghost-on-kubernetes + labels: + app: ghost-on-kubernetes + spec: + automountServiceAccountToken: false # Disable automounting of service account token + volumes: + - name: k8s-ghost-content + persistentVolumeClaim: + claimName: k8s-ghost-content + + - name: ghost-config-prod + secret: + secretName: ghost-config-prod + defaultMode: 420 + + - name: tmp + emptyDir: + sizeLimit: 64Mi + + initContainers: + - name: permissions-fix + imagePullPolicy: Always + image: docker.io/busybox:stable-musl + env: + - name: GHOST_INSTALL + value: /home/nonroot/app/ghost + - name: GHOST_CONTENT + value: /home/nonroot/app/ghost/content + - name: NODE_ENV + value: production + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + resources: + limits: + cpu: 900m + memory: 1000Mi + requests: + cpu: 100m + memory: 128Mi + command: + - /bin/sh + - '-c' + - | + set -e + + export DIRS='files logs apps themes data public settings images media' + echo 'Check if base dirs exists, if not, create them' + echo "Directories to check: $DIRS" + for dir in $DIRS; do + if [ ! -d $GHOST_CONTENT/$dir ]; then + echo "Creating $GHOST_CONTENT/$dir directory" + mkdir -pv $GHOST_CONTENT/$dir || echo "Error creating $GHOST_CONTENT/$dir directory" + fi + chown -Rfv 65532:65532 $GHOST_CONTENT/$dir && echo "chown ok on $dir" || echo "Error changing ownership of $GHOST_CONTENT/$dir directory" + done + exit 0 + + + volumeMounts: + - name: k8s-ghost-content + mountPath: /home/nonroot/app/ghost/content + readOnly: false + + containers: + - name: ghost-on-kubernetes + # For development, you can use the following image: + # image: ghcr.io/sredevopsorg/ghost-on-kubernetes:latest-dev + # image: ghcr.io/sredevopsorg/ghost-on-kubernetes:main + image: ghost:bookworm + imagePullPolicy: Always + ports: + - name: ghk8s + containerPort: 2368 + protocol: TCP + + # You should uncomment the following lines in production. Change the values according to your environment. + readinessProbe: + httpGet: + path: /ghost/api/v4/admin/site/ + port: ghk8s + httpHeaders: + - name: X-Forwarded-Proto + value: https + - name: Host + value: ghost.lab.home.hrajfrisbee.cz + periodSeconds: 10 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 10 + + livenessProbe: + httpGet: + path: /ghost/api/v4/admin/site/ + port: ghk8s + httpHeaders: + - name: X-Forwarded-Proto + value: https + - name: Host + value: ghost.lab.home.hrajfrisbee.cz + periodSeconds: 300 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 1 + initialDelaySeconds: 30 + + env: + - name: NODE_ENV + value: production + - name: url + value: "https://ghost.lab.home.hrajfrisbee.cz" + - name: database__client + value: "mysql" + - name: database__connection__host + value: "ghost-on-kubernetes-mysql-service" + - name: database__connection__port + value: "3306" + - name: database__connection__user + valueFrom: + secretKeyRef: + name: ghost-on-kubernetes-mysql-env + key: MYSQL_USER + - name: database__connection__password + valueFrom: + secretKeyRef: + name: ghost-on-kubernetes-mysql-env + key: MYSQL_PASSWORD + - name: database__connection__database + value: "ghost" + - name: mail__transport + value: "SMTP" + - name: mail__options__service + value: "Gmail" + - name: mail__options__auth__user + value: "kacerr.cz@gmail.com" + - name: mail__options__auth__pass + valueFrom: + secretKeyRef: + name: ghost-config + key: gmail-app-password + - name: mail__from + value: "'Kacerr's Blog' " + + resources: + limits: + cpu: 800m + memory: 800Mi + requests: + cpu: 100m + memory: 256Mi + + volumeMounts: + - name: k8s-ghost-content + mountPath: /home/nonroot/app/ghost/content + readOnly: false + - name: ghost-config-prod + readOnly: true + mountPath: /home/nonroot/app/ghost/config.production.json + subPath: config.production.json + - name: tmp # This is the temporary volume mount to allow loading themes + mountPath: /tmp + readOnly: false + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65532 + + + restartPolicy: Always + terminationGracePeriodSeconds: 15 + dnsPolicy: ClusterFirst + # Optional: Uncomment the following to specify node selectors + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: node-role.kubernetes.io/worker + # operator: In + # values: + # - 'true' + securityContext: {} diff --git a/gitops/home-kubernetes/ghost-on-kubernetes/07-ingress.yaml b/gitops/home-kubernetes/ghost-on-kubernetes/07-ingress.yaml new file mode 100644 index 0000000..40a32e8 --- /dev/null +++ b/gitops/home-kubernetes/ghost-on-kubernetes/07-ingress.yaml @@ -0,0 +1,33 @@ +# Optional: If you have a domain name, you can create an Ingress resource to expose your Ghost blog to the internet. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ghost-on-kubernetes-ingress + namespace: ghost-on-kubernetes + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + labels: + app: ghost-on-kubernetes + app.kubernetes.io/name: ghost-on-kubernetes-ingress + app.kubernetes.io/instance: ghost-on-kubernetes + app.kubernetes.io/version: '6.0' + app.kubernetes.io/component: ingress + app.kubernetes.io/part-of: ghost-on-kubernetes + +spec: + ingressClassName: nginx + tls: + - hosts: + - ghost.lab.home.hrajfrisbee.cz + secretName: tls-secret + rules: + - host: ghost.lab.home.hrajfrisbee.cz + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: ghost-on-kubernetes-service + port: + name: ghk8s