diff --git a/README.md b/README.md index 7638edd..285fe9f 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# STACKIT University Course \ No newline at end of file +# STACKIT University Course + +This repository accompanies the course `Deploying an Application with STACKIT Kubernetes Engine`. diff --git a/module3/.gitignore b/module3/.gitignore new file mode 100644 index 0000000..65cb323 --- /dev/null +++ b/module3/.gitignore @@ -0,0 +1 @@ +jwt.key diff --git a/module3/README.md b/module3/README.md new file mode 100644 index 0000000..c8bca57 --- /dev/null +++ b/module3/README.md @@ -0,0 +1,91 @@ +# Scrumlr K8s Deployment + +## Generate JWT Key + +```bash +openssl ecparam -genkey -name secp521r1 -noout -out $(git rev-parse --show-toplevel)/module3/k8s/kustomize/scrumlr/jwt.key +``` + +## Differences in deployment between local minikube cluster and SKE cluster + +* Skip PostgreSQL Deployment for SKE deployment, since we use external PostgreSQL +* Set the right `SCRUMLR_SERVER_DATABASE_URL` value in the scrumlr-backend secret +* Minikube: Ingress without Host and external-dns/ cert-manager annotations +* SKE: Ingress with Host and external-dns/ cert-manager annotations + +## Deploy Scrumlr into local cluster using minikube + +Local Deployment that can run on a lokal K8s cluster like a [minikube](https://minikube.sigs.k8s.io/docs/) cluster. +As we don't have Postgres as an external service here, we also install Postgres directly in our local cluster. + +```sh +cd $(git rev-parse --show-toplevel)/module3/k8s/ + +# Create Cluster with kind and start cloud-provider-kind for Ingress +minikube start +minikube tunnel + +# Install ingress-nginx and nats with helm + +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +helm repo add nats https://nats-io.github.io/k8s/helm/charts/ +helm repo update +helm upgrade \ + ingress-nginx ingress-nginx/ingress-nginx \ + --install \ + --namespace ingress-nginx \ + --create-namespace \ + --version 4.12.1 \ + --values helm/ingress-nginx/values.yaml +helm upgrade \ + --install \ + nats nats/nats \ + --namespace nats \ + --create-namespace \ + --version 1.3.3 \ + --values helm/nats/values.yaml + + +# Install kustomize applications + +kubectl apply -k kustomize/postgres +kubectl apply -k kustomize/scrumlr + +# Use external IP of your Ingress Controller in your Browser to open Scrumlr +kubectl get services --namespace ingress-nginx ingress-nginx-controller --output jsonpath='{.status.loadBalancer.ingress[0].ip}' + +# Destroy Cluster +minikube stop +minikube delete +``` + +```shell +# Optional: Connect to your PostgreSQL instance with psql (psql must be installed) +kubectl port-forward -n postgres services/postgres-scrumlr +psql -h localhost -U scrumlr -d scrumlr +# List all tables +scrumlr-# \dt + List of relations + Schema | Name | Type | Owner +--------+------------------------+-------+--------- + public | apple_users | table | scrumlr + public | azure_ad_users | table | scrumlr + public | board_session_requests | table | scrumlr + public | board_sessions | table | scrumlr + public | board_templates | table | scrumlr + public | boards | table | scrumlr + public | column_templates | table | scrumlr + public | columns | table | scrumlr + public | deleted_boards | table | scrumlr + public | github_users | table | scrumlr + public | google_users | table | scrumlr + public | microsoft_users | table | scrumlr + public | notes | table | scrumlr + public | oidc_users | table | scrumlr + public | reactions | table | scrumlr + public | schema_migrations | table | scrumlr + public | users | table | scrumlr + public | votes | table | scrumlr + public | votings | table | scrumlr +(19 rows) +``` diff --git a/module3/k8s/helm/ingress-nginx/README.md b/module3/k8s/helm/ingress-nginx/README.md new file mode 100644 index 0000000..dd8e313 --- /dev/null +++ b/module3/k8s/helm/ingress-nginx/README.md @@ -0,0 +1,15 @@ +# ingress-nginx + +Docs: https://kubernetes.github.io/ingress-nginx/ +Github Repo: https://github.com/kubernetes/ingress-nginx + +```sh +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update +helm upgrade \ + ingress-nginx ingress-nginx/ingress-nginx \ + --install \ + --namespace ingress-nginx \ + --create-namespace \ + --version 4.12.1 \ + --values values.yaml +``` diff --git a/module3/k8s/helm/ingress-nginx/values.yaml b/module3/k8s/helm/ingress-nginx/values.yaml new file mode 100644 index 0000000..6874239 --- /dev/null +++ b/module3/k8s/helm/ingress-nginx/values.yaml @@ -0,0 +1 @@ +# Note: This file is intentionally empty and is more of a placeholder to ensure consistency. diff --git a/module3/k8s/helm/nats/README.md b/module3/k8s/helm/nats/README.md new file mode 100644 index 0000000..29987e4 --- /dev/null +++ b/module3/k8s/helm/nats/README.md @@ -0,0 +1,15 @@ +# NATS + +Docs: https://docs.nats.io/ +Github Repo: https://github.com/nats-io/k8s + +```sh +helm repo add nats https://nats-io.github.io/k8s/helm/charts/ --force-update +helm upgrade \ + --install \ + nats nats/nats \ + --namespace nats \ + --create-namespace \ + --version 1.3.3 \ + --values values.yaml +``` diff --git a/module3/k8s/helm/nats/values.yaml b/module3/k8s/helm/nats/values.yaml new file mode 100644 index 0000000..b3b0d88 --- /dev/null +++ b/module3/k8s/helm/nats/values.yaml @@ -0,0 +1,4 @@ +config: + cluster: + enabled: true + replicas: 3 diff --git a/module3/k8s/kustomize/postgres/kustomization.yaml b/module3/k8s/kustomize/postgres/kustomization.yaml new file mode 100644 index 0000000..3888a2d --- /dev/null +++ b/module3/k8s/kustomize/postgres/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace-postgres.yaml + - statefulset-postgres-scrumlr.yaml + - service-postgres-scrumlr.yaml + - persistentvolumeclaim-postgres-scrumlr.yaml + - secret-postgres-scrumlr.yaml diff --git a/module3/k8s/kustomize/postgres/namespace-postgres.yaml b/module3/k8s/kustomize/postgres/namespace-postgres.yaml new file mode 100644 index 0000000..427794c --- /dev/null +++ b/module3/k8s/kustomize/postgres/namespace-postgres.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: postgres diff --git a/module3/k8s/kustomize/postgres/persistentvolumeclaim-postgres-scrumlr.yaml b/module3/k8s/kustomize/postgres/persistentvolumeclaim-postgres-scrumlr.yaml new file mode 100644 index 0000000..ac8e5d3 --- /dev/null +++ b/module3/k8s/kustomize/postgres/persistentvolumeclaim-postgres-scrumlr.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-scrumlr + namespace: postgres + labels: + app.kubernetes.io/name: "postgres" + app.kubernetes.io/component: "database" + app.kubernetes.io/part-of: "scrumlr" +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi diff --git a/module3/k8s/kustomize/postgres/secret-postgres-scrumlr.yaml b/module3/k8s/kustomize/postgres/secret-postgres-scrumlr.yaml new file mode 100644 index 0000000..c78db49 --- /dev/null +++ b/module3/k8s/kustomize/postgres/secret-postgres-scrumlr.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-scrumlr + namespace: postgres + labels: + app.kubernetes.io/name: "postgres" + app.kubernetes.io/component: "database" + app.kubernetes.io/part-of: "scrumlr" +type: Opaque +data: + # echo -n 'super-secret-password' | base64 + POSTGRES_USER: c2NydW1scg== # base64 encoded value of 'scrumlr' + POSTGRES_PASSWORD: c3VwZXItc2VjcmV0LXBhc3N3b3Jk # base64 encoded value of 'super-secret-password' diff --git a/module3/k8s/kustomize/postgres/service-postgres-scrumlr.yaml b/module3/k8s/kustomize/postgres/service-postgres-scrumlr.yaml new file mode 100644 index 0000000..1d75608 --- /dev/null +++ b/module3/k8s/kustomize/postgres/service-postgres-scrumlr.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres-scrumlr + namespace: postgres + labels: + app.kubernetes.io/name: "postgres" + app.kubernetes.io/component: "database" + app.kubernetes.io/part-of: "scrumlr" +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + selector: + app.kubernetes.io/name: "postgres" + app.kubernetes.io/component: "database" + app.kubernetes.io/part-of: "scrumlr" diff --git a/module3/k8s/kustomize/postgres/statefulset-postgres-scrumlr.yaml b/module3/k8s/kustomize/postgres/statefulset-postgres-scrumlr.yaml new file mode 100644 index 0000000..c33deca --- /dev/null +++ b/module3/k8s/kustomize/postgres/statefulset-postgres-scrumlr.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres-scrumlr + namespace: postgres + labels: + app.kubernetes.io/name: "postgres" + app.kubernetes.io/component: "database" + app.kubernetes.io/part-of: "scrumlr" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: "postgres" + app.kubernetes.io/component: "database" + app.kubernetes.io/part-of: "scrumlr" + serviceName: "postgres-scrumlr" + template: + metadata: + labels: + app.kubernetes.io/name: "postgres" + app.kubernetes.io/component: "database" + app.kubernetes.io/part-of: "scrumlr" + spec: + containers: + - name: postgres + image: postgres:17.4 + resources: + requests: + memory: "256Mi" + cpu: "20m" + limits: + memory: "256Mi" + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-scrumlr + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-scrumlr + key: POSTGRES_PASSWORD + ports: + - containerPort: 5432 + volumeMounts: + - name: postgres + mountPath: /var/lib/postgresql/data + subPath: postgres + volumes: + - name: postgres + persistentVolumeClaim: + claimName: postgres-scrumlr diff --git a/module3/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.yaml b/module3/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.yaml new file mode 100644 index 0000000..cfe1824 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: scrumlr-backend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" +data: + SCRUMLR_SERVER_PORT: "8080" + SCRUMLR_BASE_PATH: "/api" + SCRUMLR_SERVER_NATS_URL: "nats.nats.svc.cluster.local:4222" diff --git a/module3/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml b/module3/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml new file mode 100644 index 0000000..763ea31 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scrumlr-backend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + template: + metadata: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + spec: + containers: + - name: backend + image: ghcr.io/inovex/scrumlr.io/scrumlr-server:3.10.3 + args: + - "/app/main" + - "-disable-check-origin" + resources: + requests: + cpu: "50m" + memory: "200Mi" + limits: + memory: "200Mi" + startupProbe: + httpGet: + path: /api/health + port: 8080 + failureThreshold: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: 8080 + readinessProbe: + httpGet: + path: /api/health + port: 8080 + envFrom: + - configMapRef: + name: scrumlr-backend + - secretRef: + name: scrumlr-backend + env: + - name: SCRUMLR_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: scrumlr-ecdsa-key + key: jwt.key + ports: + - containerPort: 8080 diff --git a/module3/k8s/kustomize/scrumlr/backend/kustomization.yaml b/module3/k8s/kustomize/scrumlr/backend/kustomization.yaml new file mode 100644 index 0000000..90e3931 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/backend/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - configmap-scrumlr-backend.yaml + - secret-scrumlr-backend.yaml + - deployment-scrumlr-backend.yaml + - service-scrumlr-backend.yaml + - poddisruptionbudget-backend.yaml diff --git a/module3/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml b/module3/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml new file mode 100644 index 0000000..1b0542e --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: backend +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" diff --git a/module3/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.yaml b/module3/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.yaml new file mode 100644 index 0000000..e82449c --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: scrumlr-backend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" +type: Opaque +data: + SCRUMLR_SERVER_DATABASE_URL: "cG9zdGdyZXM6Ly9zY3J1bWxyOnN1cGVyLXNlY3JldC1wYXNzd29yZEBwb3N0Z3Jlcy1zY3J1bWxyLnBvc3RncmVzLnN2Yy5jbHVzdGVyLmxvY2FsOjU0MzIvc2NydW1scj9zc2xtb2RlPWRpc2FibGU=" # echo -n 'postgres://scrumlr:super-secret-password@postgres-scrumlr.postgres.svc.cluster.local:5432/scrumlr?sslmode=disable' | base64 diff --git a/module3/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml b/module3/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml new file mode 100644 index 0000000..540a2b9 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: scrumlr-backend +spec: + selector: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + ports: + - port: 8080 + targetPort: 8080 diff --git a/module3/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.yaml b/module3/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.yaml new file mode 100644 index 0000000..1fae40f --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: scrumlr-frontend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" +data: + SCRUMLR_SERVER_URL: "/api" + SCRUMLR_SERVER_PORT: "8080" + SCRUMLR_SHOW_LEGAL_DOCUMENTS: "true" diff --git a/module3/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml b/module3/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml new file mode 100644 index 0000000..d6ac47e --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scrumlr-frontend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + template: + metadata: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + spec: + containers: + - name: frontend + image: ghcr.io/inovex/scrumlr.io/scrumlr-frontend:3.10.3 + resources: + requests: + cpu: "25m" + memory: "100Mi" + limits: + memory: "100Mi" + envFrom: + - configMapRef: + name: scrumlr-frontend + ports: + - containerPort: 8080 diff --git a/module3/k8s/kustomize/scrumlr/frontend/kustomization.yaml b/module3/k8s/kustomize/scrumlr/frontend/kustomization.yaml new file mode 100644 index 0000000..d68fe0a --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/frontend/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - configmap-scrumlr-frontend.yaml + - deployment-scrumlr-frontend.yaml + - service-scrumlr-frontend.yaml + - poddisruptionbudget-frontend.yaml diff --git a/module3/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml b/module3/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml new file mode 100644 index 0000000..1ea7ab0 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: frontend +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" diff --git a/module3/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml b/module3/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml new file mode 100644 index 0000000..9bdc1e6 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: scrumlr-frontend +spec: + selector: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + ports: + - port: 80 + targetPort: 8080 diff --git a/module3/k8s/kustomize/scrumlr/ingress-scrumlr.yaml b/module3/k8s/kustomize/scrumlr/ingress-scrumlr.yaml new file mode 100644 index 0000000..06c4342 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/ingress-scrumlr.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: scrumlr + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/part-of: "scrumlr" + annotations: + nginx.ingress.kubernetes.io/limit-connections: "100" + # Websocket optimization https://kubernetes.github.io/ingress-nginx/user-guide/miscellaneous/#websockets + nginx.ingress.kubernetes.io/proxy-send-timeout: "7200" + nginx.ingress.kubernetes.io/proxy-read-timeout: "7200" +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: scrumlr-backend + port: + number: 8080 + - path: / + pathType: Prefix + backend: + service: + name: scrumlr-frontend + port: + number: 80 diff --git a/module3/k8s/kustomize/scrumlr/kustomization.yaml b/module3/k8s/kustomize/scrumlr/kustomization.yaml new file mode 100644 index 0000000..13e9042 --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: scrumlr +resources: + - namespace-scrumlr.yaml + - backend/ + - frontend/ + - ingress-scrumlr.yaml + +# Create key: openssl ecparam -genkey -name secp521r1 -noout -out jwt.key +secretGenerator: +- name: scrumlr-ecdsa-key + files: + - jwt.key diff --git a/module3/k8s/kustomize/scrumlr/namespace-scrumlr.yaml b/module3/k8s/kustomize/scrumlr/namespace-scrumlr.yaml new file mode 100644 index 0000000..f29f12e --- /dev/null +++ b/module3/k8s/kustomize/scrumlr/namespace-scrumlr.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: scrumlr diff --git a/module4/.gitignore b/module4/.gitignore new file mode 100644 index 0000000..66b9850 --- /dev/null +++ b/module4/.gitignore @@ -0,0 +1,13 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Secrets +sa_key.json +config.s3.tfbackend + +# Var file +terraform.tfvars diff --git a/module4/tf/.terraform.lock.hcl b/module4/tf/.terraform.lock.hcl new file mode 100644 index 0000000..a5e6920 --- /dev/null +++ b/module4/tf/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/stackitcloud/stackit" { + version = "0.50.0" + constraints = "0.50.0" + hashes = [ + "h1:uU8/DLvW8tEty0PI2sUMem43IDNSrncHuLaXaEYdGFk=", + "zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f", + "zh:219d678bc471b3f5030724dcdde6be3f3fa63e911b7c7a0f446b0a0b4e5f48e7", + "zh:4cd09155a09e320b0b68db4ba2971564f3d147c19ad991d6b7e731e26034d91c", + "zh:507c1e24432f0d455ac8b628c37ee20db62e89a6e85508568c2820ba52404786", + "zh:5aa10bfc4baad277a2bc746c83fca19911bb95a5e8821dd46f333bc621cbb453", + "zh:67b55ad1135ca12997b0928cb67973b11ea196299e0cf66e3e0145faec762644", + "zh:6d1d108edcd6794a8839d849e6ea48699875e22afeea7edd38bee3dd56dea7e8", + "zh:7473c28b3781c0d00294d985bd067e753a419ca8e379f91a8f6f2ce4663566ee", + "zh:8d234b24734f950f986322a5f084ca23bfd9b3d9fb7742b54404171cfcabc99e", + "zh:af0804ea918648600cc6300dffce8a7b9115d30dc88db10f962b8e596d1465e1", + "zh:b557940dc6387dc4cce8b100981ccaadac6bc4e6b50c566baf148d67939f8f2e", + "zh:d477f77ce6f807d60069c1efcfa20607088ae7ab91d22805331a7634d84c2d1c", + "zh:d95086e2338ceed511e798a2acc6d5cefdfff1a14f7b47b2d29b4ebc36b77a3a", + "zh:ea0d8d5c9cf7d5871a54dd4786c378dfd9d10416f3c4d0ea4776465e8c562e10", + "zh:f96af7b89dc99745f6a22c0ca2aedb18e10273251b8cbac9e2b1011c68c3c3f9", + ] +} diff --git a/module4/tf/README.md b/module4/tf/README.md new file mode 100644 index 0000000..b926a44 --- /dev/null +++ b/module4/tf/README.md @@ -0,0 +1,37 @@ +# Terraform + +## Create service account + +* Install CLI tool: https://github.com/stackitcloud/stackit-cli/blob/main/INSTALLATION.md +* `stackit auth login` +* `stackit project list` +* `stackit config set --project-id PROJECTx-IDyy-zzzz-aaaa-DUMMYbbbbbbb` +* `stackit service-account create --name terraform` +* `stackit service-account list` + +* `stackit service-account key create --email terraform-vsPzcS7@sa.stackit.cloud > sa_key.json` +* `stackit service-account key list --email terraform-vsPzcS7@sa.stackit.cloud` + +* `stackit project member add terraform-vsPzcS7@sa.stackit.cloud --role editor` + +## S3 Backend for tfstate + +`stackit object-storage enable` + +Note: The name must be globally unique. Use only lowercase letters, numbers or hyphens. The name should be at least 3 and at most 63 characters long. + +Lets use something kinda random, to have a higher chance of catching an free bucket name: + +* `stackit object-storage bucket create tfstate-bucket-g5el` +* `stackit object-storage credentials-group create --name terraform-state` +* `stackit object-storage credentials create --credentials-group-id CREDGROU-Pxxx-IDzz-aaaa-DUMMYbbbbbbb` + +* `terraform init --backend-config=./config.s3.tfbackend` + +Now we can start using Terraform. + +## Connect to created SKE cluster + +* `stackit ske cluster list` +* `stackit ske kubeconfig create scrumlr --login` +* `--login` ensures that authentication is performed with the stackit CLI and very short-lived credentials are used in the background, without this flag the credentials are static and and usually have a longer lifetime. If the credentials are obtained without the `--login` flag, they must be renewed manually. diff --git a/module4/tf/config.s3.tfbackend.example b/module4/tf/config.s3.tfbackend.example new file mode 100644 index 0000000..14e1476 --- /dev/null +++ b/module4/tf/config.s3.tfbackend.example @@ -0,0 +1,7 @@ +# Replace keys and bucket name here and rename this file to config.s3.tfbackend +secret_key = "xxx" +access_key = "yyy" +bucket = "tfstate-bucket-SUFFIX" + +# The key can be left as it is, but can also be customized as desired (before the terraform init) +key = "scrumlr.tfstate" diff --git a/module4/tf/dns.tf b/module4/tf/dns.tf new file mode 100644 index 0000000..813cac2 --- /dev/null +++ b/module4/tf/dns.tf @@ -0,0 +1,5 @@ +resource "stackit_dns_zone" "scrumlr" { + project_id = var.project_id + dns_name = var.dns_name + name = "Scrumlr Zone" +} diff --git a/module4/tf/main.tf b/module4/tf/main.tf new file mode 100644 index 0000000..5071f9f --- /dev/null +++ b/module4/tf/main.tf @@ -0,0 +1,36 @@ +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "0.50.0" + } + } + backend "s3" { + + # Secrets and config outsourced to config.s3.tfbackend file, which is included in .gitignore + # See also: https://developer.hashicorp.com/terraform/language/backend#partial-configuration + # terraform init --backend-config=./config.s3.tfbackend + #bucket = "tfstate-bucket-SUFFIX" + #key = "scrumlr.tfstate" + #secret_key = "SECRETKEY" + #access_key = "ACCESSKEY" + + endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" + } + region = "eu01" + + # Also use remote locking + use_lockfile = true + + # AWS specific checks must be skipped as they do not work on STACKIT. + skip_credentials_validation = true + skip_region_validation = true + skip_s3_checksum = true + skip_requesting_account_id = true + } +} +provider "stackit" { + default_region = "eu01" + service_account_key_path = "sa_key.json" +} diff --git a/module4/tf/postgres.tf b/module4/tf/postgres.tf new file mode 100644 index 0000000..4d00402 --- /dev/null +++ b/module4/tf/postgres.tf @@ -0,0 +1,42 @@ +resource "stackit_postgresflex_instance" "scrumlr" { + project_id = var.project_id + name = "scrumlr" + acl = stackit_ske_cluster.scrumlr.egress_address_ranges + backup_schedule = "00 00 * * *" + flavor = { + cpu = 2 + ram = 4 + } + replicas = 3 + storage = { + class = "premium-perf6-stackit" + size = 5 + } + version = 17 +} + +resource "stackit_postgresflex_user" "scrumlr" { + project_id = var.project_id + instance_id = stackit_postgresflex_instance.scrumlr.instance_id + username = "scrumlr" + roles = ["login", "createdb"] +} + +resource "stackit_postgresflex_database" "scrumlr" { + project_id = var.project_id + instance_id = stackit_postgresflex_instance.scrumlr.instance_id + owner = stackit_postgresflex_user.scrumlr.username + name = "scrumlr" +} + +output "postgres_dsn" { + value = format( + "postgres://%s:%s@%s:%d/%s", + stackit_postgresflex_user.scrumlr.username, + stackit_postgresflex_user.scrumlr.password, + stackit_postgresflex_user.scrumlr.host, + stackit_postgresflex_user.scrumlr.port, + stackit_postgresflex_database.scrumlr.name + ) + sensitive = true +} diff --git a/module4/tf/ske.tf b/module4/tf/ske.tf new file mode 100644 index 0000000..84f5b75 --- /dev/null +++ b/module4/tf/ske.tf @@ -0,0 +1,28 @@ +resource "stackit_ske_cluster" "scrumlr" { + project_id = var.project_id + name = "scrumlr" + kubernetes_version_min = "1.31.7" + node_pools = [ + { + name = "scrumlrpool" + machine_type = "c1.3" + os_name = "flatcar" + minimum = "3" + maximum = "3" + availability_zones = ["eu01-1", "eu01-2", "eu01-3"] + volume_type = "storage_premium_perf2" + } + ] + maintenance = { + enable_kubernetes_version_updates = true + enable_machine_image_version_updates = true + start = "01:00:00Z" + end = "02:00:00Z" + } + extensions = { + dns = { + enabled = true + zones = [stackit_dns_zone.scrumlr.dns_name] + } + } +} diff --git a/module4/tf/terraform.tfvars.example b/module4/tf/terraform.tfvars.example new file mode 100644 index 0000000..0beda82 --- /dev/null +++ b/module4/tf/terraform.tfvars.example @@ -0,0 +1,2 @@ +project_id = "PROJECTx-IDyy-zzzz-aaaa-DUMMYbbbbbbb" # CHANGE-ME +dns_name = "CHANGE-ME.stackit.gg" # CHANGE-ME diff --git a/module4/tf/variables.tf b/module4/tf/variables.tf new file mode 100644 index 0000000..1bbb60c --- /dev/null +++ b/module4/tf/variables.tf @@ -0,0 +1,9 @@ +variable "project_id" { + description = "The STACKIT project ID" + type = string +} + +variable "dns_name" { + description = "DNS name for generated Zone" + type = string +} diff --git a/module5/.gitignore b/module5/.gitignore new file mode 100644 index 0000000..9e6cd8d --- /dev/null +++ b/module5/.gitignore @@ -0,0 +1,4 @@ +jwt.key + +secret-scrumlr-backend.yaml +ingress-scrumlr.yaml diff --git a/module5/k8s/helm/cert-manager/README.md b/module5/k8s/helm/cert-manager/README.md new file mode 100644 index 0000000..35c985d --- /dev/null +++ b/module5/k8s/helm/cert-manager/README.md @@ -0,0 +1,21 @@ +# cert-manager + +Docs: https://cert-manager.io/docs/ +Github Repo: https://github.com/cert-manager/cert-manager + +```sh +helm repo add jetstack https://charts.jetstack.io --force-update +helm upgrade \ + --install \ + cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --version v1.17.2 \ + --values values.yaml +``` + +Install [ClusterIssuer](https://cert-manager.io/docs/concepts/issuer/) for [Let's Encrypt](https://letsencrypt.org/) + +```sh +kubectl apply -f clusterissuer-letsencrypt-production.yaml +``` diff --git a/module5/k8s/helm/cert-manager/clusterissuer-letsencrypt-production.yaml b/module5/k8s/helm/cert-manager/clusterissuer-letsencrypt-production.yaml new file mode 100644 index 0000000..51a6d6b --- /dev/null +++ b/module5/k8s/helm/cert-manager/clusterissuer-letsencrypt-production.yaml @@ -0,0 +1,13 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-production + solvers: + - http01: + ingress: + ingressClassName: nginx diff --git a/module5/k8s/helm/cert-manager/values.yaml b/module5/k8s/helm/cert-manager/values.yaml new file mode 100644 index 0000000..c526c4b --- /dev/null +++ b/module5/k8s/helm/cert-manager/values.yaml @@ -0,0 +1,2 @@ +crds: + enabled: true diff --git a/module5/k8s/helm/ingress-nginx/README.md b/module5/k8s/helm/ingress-nginx/README.md new file mode 100644 index 0000000..dd8e313 --- /dev/null +++ b/module5/k8s/helm/ingress-nginx/README.md @@ -0,0 +1,15 @@ +# ingress-nginx + +Docs: https://kubernetes.github.io/ingress-nginx/ +Github Repo: https://github.com/kubernetes/ingress-nginx + +```sh +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update +helm upgrade \ + ingress-nginx ingress-nginx/ingress-nginx \ + --install \ + --namespace ingress-nginx \ + --create-namespace \ + --version 4.12.1 \ + --values values.yaml +``` diff --git a/module5/k8s/helm/ingress-nginx/values.yaml b/module5/k8s/helm/ingress-nginx/values.yaml new file mode 100644 index 0000000..6874239 --- /dev/null +++ b/module5/k8s/helm/ingress-nginx/values.yaml @@ -0,0 +1 @@ +# Note: This file is intentionally empty and is more of a placeholder to ensure consistency. diff --git a/module5/k8s/helm/nats/README.md b/module5/k8s/helm/nats/README.md new file mode 100644 index 0000000..29987e4 --- /dev/null +++ b/module5/k8s/helm/nats/README.md @@ -0,0 +1,15 @@ +# NATS + +Docs: https://docs.nats.io/ +Github Repo: https://github.com/nats-io/k8s + +```sh +helm repo add nats https://nats-io.github.io/k8s/helm/charts/ --force-update +helm upgrade \ + --install \ + nats nats/nats \ + --namespace nats \ + --create-namespace \ + --version 1.3.3 \ + --values values.yaml +``` diff --git a/module5/k8s/helm/nats/values.yaml b/module5/k8s/helm/nats/values.yaml new file mode 100644 index 0000000..fcd4517 --- /dev/null +++ b/module5/k8s/helm/nats/values.yaml @@ -0,0 +1,10 @@ +config: + cluster: + enabled: true + replicas: 3 + +podTemplate: + topologySpreadConstraints: + kubernetes.io/hostname: + maxSkew: 1 + whenUnsatisfiable: ScheduleAnyway diff --git a/module5/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.yaml b/module5/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.yaml new file mode 100644 index 0000000..cfe1824 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: scrumlr-backend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" +data: + SCRUMLR_SERVER_PORT: "8080" + SCRUMLR_BASE_PATH: "/api" + SCRUMLR_SERVER_NATS_URL: "nats.nats.svc.cluster.local:4222" diff --git a/module5/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml b/module5/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml new file mode 100644 index 0000000..f832dcf --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scrumlr-backend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + template: + metadata: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + containers: + - name: backend + image: ghcr.io/inovex/scrumlr.io/scrumlr-server:3.10.3 + args: + - "/app/main" + - "-disable-check-origin" + resources: + requests: + cpu: "50m" + memory: "200Mi" + limits: + memory: "200Mi" + startupProbe: + httpGet: + path: /api/health + port: 8080 + failureThreshold: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: 8080 + readinessProbe: + httpGet: + path: /api/health + port: 8080 + envFrom: + - configMapRef: + name: scrumlr-backend + - secretRef: + name: scrumlr-backend + env: + - name: SCRUMLR_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: scrumlr-ecdsa-key + key: jwt.key + ports: + - containerPort: 8080 diff --git a/module5/k8s/kustomize/scrumlr/backend/kustomization.yaml b/module5/k8s/kustomize/scrumlr/backend/kustomization.yaml new file mode 100644 index 0000000..90e3931 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/backend/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - configmap-scrumlr-backend.yaml + - secret-scrumlr-backend.yaml + - deployment-scrumlr-backend.yaml + - service-scrumlr-backend.yaml + - poddisruptionbudget-backend.yaml diff --git a/module5/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml b/module5/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml new file mode 100644 index 0000000..1b0542e --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: backend +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" diff --git a/module5/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.yaml.example b/module5/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.yaml.example new file mode 100644 index 0000000..cb5e560 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.yaml.example @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: scrumlr-backend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" +type: Opaque +data: + # echo -n 'postgres://user:PASSWORD@INSTANCE-ID.postgresql.eu01.onstackit.cloud:5432/scrumlr' | base64 + SCRUMLR_SERVER_DATABASE_URL: "CHANGE-ME" diff --git a/module5/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml b/module5/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml new file mode 100644 index 0000000..540a2b9 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: scrumlr-backend +spec: + selector: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + ports: + - port: 8080 + targetPort: 8080 diff --git a/module5/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.yaml b/module5/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.yaml new file mode 100644 index 0000000..1fae40f --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: scrumlr-frontend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" +data: + SCRUMLR_SERVER_URL: "/api" + SCRUMLR_SERVER_PORT: "8080" + SCRUMLR_SHOW_LEGAL_DOCUMENTS: "true" diff --git a/module5/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml b/module5/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml new file mode 100644 index 0000000..a60c28d --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scrumlr-frontend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + template: + metadata: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + containers: + - name: frontend + image: ghcr.io/inovex/scrumlr.io/scrumlr-frontend:3.10.3 + resources: + requests: + cpu: "25m" + memory: "100Mi" + limits: + memory: "100Mi" + envFrom: + - configMapRef: + name: scrumlr-frontend + ports: + - containerPort: 8080 diff --git a/module5/k8s/kustomize/scrumlr/frontend/kustomization.yaml b/module5/k8s/kustomize/scrumlr/frontend/kustomization.yaml new file mode 100644 index 0000000..d68fe0a --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/frontend/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - configmap-scrumlr-frontend.yaml + - deployment-scrumlr-frontend.yaml + - service-scrumlr-frontend.yaml + - poddisruptionbudget-frontend.yaml diff --git a/module5/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml b/module5/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml new file mode 100644 index 0000000..1ea7ab0 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: frontend +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" diff --git a/module5/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml b/module5/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml new file mode 100644 index 0000000..9bdc1e6 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: scrumlr-frontend +spec: + selector: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + ports: + - port: 80 + targetPort: 8080 diff --git a/module5/k8s/kustomize/scrumlr/ingress-scrumlr.yaml.example b/module5/k8s/kustomize/scrumlr/ingress-scrumlr.yaml.example new file mode 100644 index 0000000..7a832d7 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/ingress-scrumlr.yaml.example @@ -0,0 +1,37 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: scrumlr + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/part-of: "scrumlr" + annotations: + nginx.ingress.kubernetes.io/limit-connections: "100" + # Websocket optimization https://kubernetes.github.io/ingress-nginx/user-guide/miscellaneous/#websockets + nginx.ingress.kubernetes.io/proxy-send-timeout: "7200" + nginx.ingress.kubernetes.io/proxy-read-timeout: "7200" + cert-manager.io/cluster-issuer: "letsencrypt-production" +spec: + ingressClassName: nginx + tls: + - hosts: + - CHANGE-ME.domain.tld + secretName: scrumlr-tls + rules: + - host: CHANGE-ME.domain.tld + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: scrumlr-backend + port: + number: 8080 + - path: / + pathType: Prefix + backend: + service: + name: scrumlr-frontend + port: + number: 80 diff --git a/module5/k8s/kustomize/scrumlr/kustomization.yaml b/module5/k8s/kustomize/scrumlr/kustomization.yaml new file mode 100644 index 0000000..13e9042 --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: scrumlr +resources: + - namespace-scrumlr.yaml + - backend/ + - frontend/ + - ingress-scrumlr.yaml + +# Create key: openssl ecparam -genkey -name secp521r1 -noout -out jwt.key +secretGenerator: +- name: scrumlr-ecdsa-key + files: + - jwt.key diff --git a/module5/k8s/kustomize/scrumlr/namespace-scrumlr.yaml b/module5/k8s/kustomize/scrumlr/namespace-scrumlr.yaml new file mode 100644 index 0000000..f29f12e --- /dev/null +++ b/module5/k8s/kustomize/scrumlr/namespace-scrumlr.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: scrumlr diff --git a/module6/.gitignore b/module6/.gitignore new file mode 100644 index 0000000..189d295 --- /dev/null +++ b/module6/.gitignore @@ -0,0 +1,20 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Secrets +sa_key.json +config.s3.tfbackend + +# Var file +terraform.tfvars + +jwt.key + +ingress-scrumlr.yaml +secret-scrumlr-backend.env + +k8s/helm/alloy/values.yaml diff --git a/module6/k8s/helm/alloy/README.md b/module6/k8s/helm/alloy/README.md new file mode 100644 index 0000000..38fadb0 --- /dev/null +++ b/module6/k8s/helm/alloy/README.md @@ -0,0 +1,16 @@ +# Grafana Alloy + +Docs: https://grafana.com/docs/alloy/latest/ +Github Repo: https://github.com/grafana/alloy + +```shell +cp values.yaml.example values.yaml +helm repo add grafana https://grafana.github.io/helm-charts --force-update +helm upgrade \ + --install \ + alloy grafana/alloy \ + --namespace o11y \ + --create-namespace \ + --version 1.0.2 \ + --values values.yaml +``` diff --git a/module6/k8s/helm/alloy/values.yaml.example b/module6/k8s/helm/alloy/values.yaml.example new file mode 100644 index 0000000..2b96eaf --- /dev/null +++ b/module6/k8s/helm/alloy/values.yaml.example @@ -0,0 +1,154 @@ +alloy: + configMap: + content: |- + // Write your Alloy config here: + logging { + level = "info" + format = "logfmt" + } + + prometheus.remote_write "default" { + endpoint { + url = + basic_auth { + username = + password = + } + } + } + + loki.write "default" { + endpoint { + url = + basic_auth { + username = + password = + } + } + } + + prometheus.operator.podmonitors "services" { + forward_to = [prometheus.remote_write.default.receiver] + } + + prometheus.operator.servicemonitors "services" { + forward_to = [prometheus.remote_write.default.receiver] + } + + // discovery.kubernetes allows you to find scrape targets from Kubernetes resources. + // It watches cluster state and ensures targets are continually synced with what is currently running in your cluster. + discovery.kubernetes "pod" { + role = "pod" + } + + // discovery.relabel rewrites the label set of the input targets by applying one or more relabeling rules. + // If no rules are defined, then the input targets are exported as-is. + discovery.relabel "pod_logs" { + targets = discovery.kubernetes.pod.targets + + // Label creation - "namespace" field from "__meta_kubernetes_namespace" + rule { + source_labels = ["__meta_kubernetes_namespace"] + action = "replace" + target_label = "namespace" + } + + // Label creation - "pod" field from "__meta_kubernetes_pod_name" + rule { + source_labels = ["__meta_kubernetes_pod_name"] + action = "replace" + target_label = "pod" + } + + // Label creation - "container" field from "__meta_kubernetes_pod_container_name" + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + action = "replace" + target_label = "container" + } + + // Label creation - "app" field from "__meta_kubernetes_pod_label_app_kubernetes_io_name" + rule { + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"] + action = "replace" + target_label = "app" + } + + // Label creation - "job" field from "__meta_kubernetes_namespace" and "__meta_kubernetes_pod_container_name" + // Concatenate values __meta_kubernetes_namespace/__meta_kubernetes_pod_container_name + rule { + source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_pod_container_name"] + action = "replace" + target_label = "job" + separator = "/" + replacement = "$1" + } + + // Label creation - "container" field from "__meta_kubernetes_pod_uid" and "__meta_kubernetes_pod_container_name" + // Concatenate values __meta_kubernetes_pod_uid/__meta_kubernetes_pod_container_name.log + rule { + source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"] + action = "replace" + target_label = "__path__" + separator = "/" + replacement = "/var/log/pods/*$1/*.log" + } + + // Label creation - "container_runtime" field from "__meta_kubernetes_pod_container_id" + rule { + source_labels = ["__meta_kubernetes_pod_container_id"] + action = "replace" + target_label = "container_runtime" + regex = "^(\\S+):\\/\\/.+$" + replacement = "$1" + } + } + + // loki.source.kubernetes tails logs from Kubernetes containers using the Kubernetes API. + loki.source.kubernetes "pod_logs" { + targets = discovery.relabel.pod_logs.output + forward_to = [loki.process.pod_logs.receiver] + } + + // loki.process receives log entries from other Loki components, applies one or more processing stages, + // and forwards the results to the list of receivers in the component's arguments. + loki.process "pod_logs" { + stage.static_labels { + values = { + cluster = "scrumlr", + } + } + + forward_to = [loki.write.default.receiver] + } + + // loki.source.kubernetes_events tails events from the Kubernetes API and converts them + // into log lines to forward to other Loki components. + loki.source.kubernetes_events "cluster_events" { + job_name = "integrations/kubernetes/eventhandler" + log_format = "logfmt" + forward_to = [ + loki.process.cluster_events.receiver, + ] + } + + // loki.process receives log entries from other loki components, applies one or more processing stages, + // and forwards the results to the list of receivers in the component's arguments. + loki.process "cluster_events" { + forward_to = [loki.write.default.receiver] + + stage.static_labels { + values = { + cluster = "scrumlr", + } + } + + stage.labels { + values = { + kubernetes_cluster_events = "job", + } + } + } + + + diff --git a/module6/k8s/helm/cert-manager/README.md b/module6/k8s/helm/cert-manager/README.md new file mode 100644 index 0000000..35c985d --- /dev/null +++ b/module6/k8s/helm/cert-manager/README.md @@ -0,0 +1,21 @@ +# cert-manager + +Docs: https://cert-manager.io/docs/ +Github Repo: https://github.com/cert-manager/cert-manager + +```sh +helm repo add jetstack https://charts.jetstack.io --force-update +helm upgrade \ + --install \ + cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --version v1.17.2 \ + --values values.yaml +``` + +Install [ClusterIssuer](https://cert-manager.io/docs/concepts/issuer/) for [Let's Encrypt](https://letsencrypt.org/) + +```sh +kubectl apply -f clusterissuer-letsencrypt-production.yaml +``` diff --git a/module6/k8s/helm/cert-manager/clusterissuer-letsencrypt-production.yaml b/module6/k8s/helm/cert-manager/clusterissuer-letsencrypt-production.yaml new file mode 100644 index 0000000..51a6d6b --- /dev/null +++ b/module6/k8s/helm/cert-manager/clusterissuer-letsencrypt-production.yaml @@ -0,0 +1,13 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-production + solvers: + - http01: + ingress: + ingressClassName: nginx diff --git a/module6/k8s/helm/cert-manager/values.yaml b/module6/k8s/helm/cert-manager/values.yaml new file mode 100644 index 0000000..c526c4b --- /dev/null +++ b/module6/k8s/helm/cert-manager/values.yaml @@ -0,0 +1,2 @@ +crds: + enabled: true diff --git a/module6/k8s/helm/ingress-nginx/README.md b/module6/k8s/helm/ingress-nginx/README.md new file mode 100644 index 0000000..dd8e313 --- /dev/null +++ b/module6/k8s/helm/ingress-nginx/README.md @@ -0,0 +1,15 @@ +# ingress-nginx + +Docs: https://kubernetes.github.io/ingress-nginx/ +Github Repo: https://github.com/kubernetes/ingress-nginx + +```sh +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update +helm upgrade \ + ingress-nginx ingress-nginx/ingress-nginx \ + --install \ + --namespace ingress-nginx \ + --create-namespace \ + --version 4.12.1 \ + --values values.yaml +``` diff --git a/module6/k8s/helm/ingress-nginx/values.yaml b/module6/k8s/helm/ingress-nginx/values.yaml new file mode 100644 index 0000000..ecca08c --- /dev/null +++ b/module6/k8s/helm/ingress-nginx/values.yaml @@ -0,0 +1,5 @@ +controller: + metrics: + enabled: true + serviceMonitor: + enabled: true diff --git a/module6/k8s/helm/nats/README.md b/module6/k8s/helm/nats/README.md new file mode 100644 index 0000000..29987e4 --- /dev/null +++ b/module6/k8s/helm/nats/README.md @@ -0,0 +1,15 @@ +# NATS + +Docs: https://docs.nats.io/ +Github Repo: https://github.com/nats-io/k8s + +```sh +helm repo add nats https://nats-io.github.io/k8s/helm/charts/ --force-update +helm upgrade \ + --install \ + nats nats/nats \ + --namespace nats \ + --create-namespace \ + --version 1.3.3 \ + --values values.yaml +``` diff --git a/module6/k8s/helm/nats/values.yaml b/module6/k8s/helm/nats/values.yaml new file mode 100644 index 0000000..1659a08 --- /dev/null +++ b/module6/k8s/helm/nats/values.yaml @@ -0,0 +1,15 @@ +config: + cluster: + enabled: true + replicas: 3 + +podTemplate: + topologySpreadConstraints: + kubernetes.io/hostname: + maxSkew: 1 + whenUnsatisfiable: ScheduleAnyway + +promExporter: + enabled: true + podMonitor: + enabled: true diff --git a/module6/k8s/helm/prometheus-operator-crds/README.md b/module6/k8s/helm/prometheus-operator-crds/README.md new file mode 100644 index 0000000..929e727 --- /dev/null +++ b/module6/k8s/helm/prometheus-operator-crds/README.md @@ -0,0 +1,15 @@ +# Prometheus Operator CRDs + +Docs: https://prometheus-operator.dev/docs/getting-started/introduction/ +Github Repo: https://github.com/prometheus-community/helm-charts/tree/main/charts/prometheus-operator-crds + +```sh +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts --force-update +helm upgrade \ + --install \ + prometheus-operator-crds prometheus-community/prometheus-operator-crds \ + --namespace o11y \ + --create-namespace \ + --version 19.1.0 \ + --values values.yaml +``` diff --git a/module6/k8s/helm/prometheus-operator-crds/values.yaml b/module6/k8s/helm/prometheus-operator-crds/values.yaml new file mode 100644 index 0000000..395f37d --- /dev/null +++ b/module6/k8s/helm/prometheus-operator-crds/values.yaml @@ -0,0 +1,31 @@ +# Only enable Service & Pod Monitor +crds: + servicemonitors: + enabled: true + + podmonitors: + enabled: true + + alertmanagerconfigs: + enabled: false + + alertmanagers: + enabled: false + + probes: + enabled: false + + prometheusagents: + enabled: false + + prometheuses: + enabled: false + + prometheusrules: + enabled: false + + scrapeconfigs: + enabled: false + + thanosrulers: + enabled: false diff --git a/module6/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.env b/module6/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.env new file mode 100644 index 0000000..19e590a --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/backend/configmap-scrumlr-backend.env @@ -0,0 +1,3 @@ +SCRUMLR_SERVER_PORT=8080 +SCRUMLR_BASE_PATH=/api +SCRUMLR_SERVER_NATS_URL=nats.nats.svc.cluster.local:4222 diff --git a/module6/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml b/module6/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml new file mode 100644 index 0000000..f832dcf --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/backend/deployment-scrumlr-backend.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scrumlr-backend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + template: + metadata: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + containers: + - name: backend + image: ghcr.io/inovex/scrumlr.io/scrumlr-server:3.10.3 + args: + - "/app/main" + - "-disable-check-origin" + resources: + requests: + cpu: "50m" + memory: "200Mi" + limits: + memory: "200Mi" + startupProbe: + httpGet: + path: /api/health + port: 8080 + failureThreshold: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: 8080 + readinessProbe: + httpGet: + path: /api/health + port: 8080 + envFrom: + - configMapRef: + name: scrumlr-backend + - secretRef: + name: scrumlr-backend + env: + - name: SCRUMLR_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: scrumlr-ecdsa-key + key: jwt.key + ports: + - containerPort: 8080 diff --git a/module6/k8s/kustomize/scrumlr/backend/kustomization.yaml b/module6/k8s/kustomize/scrumlr/backend/kustomization.yaml new file mode 100644 index 0000000..ff84c94 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/backend/kustomization.yaml @@ -0,0 +1,24 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment-scrumlr-backend.yaml + - service-scrumlr-backend.yaml + - poddisruptionbudget-backend.yaml +configMapGenerator: + - name: scrumlr-backend + options: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + envs: + - configmap-scrumlr-backend.env +secretGenerator: + - name: scrumlr-backend + options: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + envs: + - secret-scrumlr-backend.env diff --git a/module6/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml b/module6/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml new file mode 100644 index 0000000..1b0542e --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/backend/poddisruptionbudget-backend.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: backend +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" diff --git a/module6/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.env.example b/module6/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.env.example new file mode 100644 index 0000000..2f5776f --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/backend/secret-scrumlr-backend.env.example @@ -0,0 +1 @@ +SCRUMLR_SERVER_DATABASE_URL=postgres://user:PASSWORD@INSTANCE-ID.postgresql.eu01.onstackit.cloud:5432/scrumlr diff --git a/module6/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml b/module6/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml new file mode 100644 index 0000000..540a2b9 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/backend/service-scrumlr-backend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: scrumlr-backend +spec: + selector: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "backend" + app.kubernetes.io/part-of: "scrumlr" + ports: + - port: 8080 + targetPort: 8080 diff --git a/module6/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.env b/module6/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.env new file mode 100644 index 0000000..c40ca07 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/frontend/configmap-scrumlr-frontend.env @@ -0,0 +1,3 @@ +SCRUMLR_SERVER_URL=/api +SCRUMLR_SERVER_PORT=8080 +SCRUMLR_SHOW_LEGAL_DOCUMENTS=true diff --git a/module6/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml b/module6/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml new file mode 100644 index 0000000..a60c28d --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/frontend/deployment-scrumlr-frontend.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scrumlr-frontend + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + template: + metadata: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + containers: + - name: frontend + image: ghcr.io/inovex/scrumlr.io/scrumlr-frontend:3.10.3 + resources: + requests: + cpu: "25m" + memory: "100Mi" + limits: + memory: "100Mi" + envFrom: + - configMapRef: + name: scrumlr-frontend + ports: + - containerPort: 8080 diff --git a/module6/k8s/kustomize/scrumlr/frontend/kustomization.yaml b/module6/k8s/kustomize/scrumlr/frontend/kustomization.yaml new file mode 100644 index 0000000..aed7797 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/frontend/kustomization.yaml @@ -0,0 +1,15 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment-scrumlr-frontend.yaml + - service-scrumlr-frontend.yaml + - poddisruptionbudget-frontend.yaml +configMapGenerator: + - name: scrumlr-frontend + options: + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + envs: + - configmap-scrumlr-frontend.env diff --git a/module6/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml b/module6/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml new file mode 100644 index 0000000..1ea7ab0 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/frontend/poddisruptionbudget-frontend.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: frontend +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" diff --git a/module6/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml b/module6/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml new file mode 100644 index 0000000..9bdc1e6 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/frontend/service-scrumlr-frontend.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: scrumlr-frontend +spec: + selector: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/component: "frontend" + app.kubernetes.io/part-of: "scrumlr" + ports: + - port: 80 + targetPort: 8080 diff --git a/module6/k8s/kustomize/scrumlr/ingress-scrumlr.yaml.example b/module6/k8s/kustomize/scrumlr/ingress-scrumlr.yaml.example new file mode 100644 index 0000000..7a832d7 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/ingress-scrumlr.yaml.example @@ -0,0 +1,37 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: scrumlr + labels: + app.kubernetes.io/name: "scrumlr" + app.kubernetes.io/part-of: "scrumlr" + annotations: + nginx.ingress.kubernetes.io/limit-connections: "100" + # Websocket optimization https://kubernetes.github.io/ingress-nginx/user-guide/miscellaneous/#websockets + nginx.ingress.kubernetes.io/proxy-send-timeout: "7200" + nginx.ingress.kubernetes.io/proxy-read-timeout: "7200" + cert-manager.io/cluster-issuer: "letsencrypt-production" +spec: + ingressClassName: nginx + tls: + - hosts: + - CHANGE-ME.domain.tld + secretName: scrumlr-tls + rules: + - host: CHANGE-ME.domain.tld + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: scrumlr-backend + port: + number: 8080 + - path: / + pathType: Prefix + backend: + service: + name: scrumlr-frontend + port: + number: 80 diff --git a/module6/k8s/kustomize/scrumlr/kustomization.yaml b/module6/k8s/kustomize/scrumlr/kustomization.yaml new file mode 100644 index 0000000..13e9042 --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: scrumlr +resources: + - namespace-scrumlr.yaml + - backend/ + - frontend/ + - ingress-scrumlr.yaml + +# Create key: openssl ecparam -genkey -name secp521r1 -noout -out jwt.key +secretGenerator: +- name: scrumlr-ecdsa-key + files: + - jwt.key diff --git a/module6/k8s/kustomize/scrumlr/namespace-scrumlr.yaml b/module6/k8s/kustomize/scrumlr/namespace-scrumlr.yaml new file mode 100644 index 0000000..f29f12e --- /dev/null +++ b/module6/k8s/kustomize/scrumlr/namespace-scrumlr.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: scrumlr diff --git a/module6/tf/.terraform.lock.hcl b/module6/tf/.terraform.lock.hcl new file mode 100644 index 0000000..a5e6920 --- /dev/null +++ b/module6/tf/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/stackitcloud/stackit" { + version = "0.50.0" + constraints = "0.50.0" + hashes = [ + "h1:uU8/DLvW8tEty0PI2sUMem43IDNSrncHuLaXaEYdGFk=", + "zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f", + "zh:219d678bc471b3f5030724dcdde6be3f3fa63e911b7c7a0f446b0a0b4e5f48e7", + "zh:4cd09155a09e320b0b68db4ba2971564f3d147c19ad991d6b7e731e26034d91c", + "zh:507c1e24432f0d455ac8b628c37ee20db62e89a6e85508568c2820ba52404786", + "zh:5aa10bfc4baad277a2bc746c83fca19911bb95a5e8821dd46f333bc621cbb453", + "zh:67b55ad1135ca12997b0928cb67973b11ea196299e0cf66e3e0145faec762644", + "zh:6d1d108edcd6794a8839d849e6ea48699875e22afeea7edd38bee3dd56dea7e8", + "zh:7473c28b3781c0d00294d985bd067e753a419ca8e379f91a8f6f2ce4663566ee", + "zh:8d234b24734f950f986322a5f084ca23bfd9b3d9fb7742b54404171cfcabc99e", + "zh:af0804ea918648600cc6300dffce8a7b9115d30dc88db10f962b8e596d1465e1", + "zh:b557940dc6387dc4cce8b100981ccaadac6bc4e6b50c566baf148d67939f8f2e", + "zh:d477f77ce6f807d60069c1efcfa20607088ae7ab91d22805331a7634d84c2d1c", + "zh:d95086e2338ceed511e798a2acc6d5cefdfff1a14f7b47b2d29b4ebc36b77a3a", + "zh:ea0d8d5c9cf7d5871a54dd4786c378dfd9d10416f3c4d0ea4776465e8c562e10", + "zh:f96af7b89dc99745f6a22c0ca2aedb18e10273251b8cbac9e2b1011c68c3c3f9", + ] +} diff --git a/module6/tf/README.md b/module6/tf/README.md new file mode 100644 index 0000000..b926a44 --- /dev/null +++ b/module6/tf/README.md @@ -0,0 +1,37 @@ +# Terraform + +## Create service account + +* Install CLI tool: https://github.com/stackitcloud/stackit-cli/blob/main/INSTALLATION.md +* `stackit auth login` +* `stackit project list` +* `stackit config set --project-id PROJECTx-IDyy-zzzz-aaaa-DUMMYbbbbbbb` +* `stackit service-account create --name terraform` +* `stackit service-account list` + +* `stackit service-account key create --email terraform-vsPzcS7@sa.stackit.cloud > sa_key.json` +* `stackit service-account key list --email terraform-vsPzcS7@sa.stackit.cloud` + +* `stackit project member add terraform-vsPzcS7@sa.stackit.cloud --role editor` + +## S3 Backend for tfstate + +`stackit object-storage enable` + +Note: The name must be globally unique. Use only lowercase letters, numbers or hyphens. The name should be at least 3 and at most 63 characters long. + +Lets use something kinda random, to have a higher chance of catching an free bucket name: + +* `stackit object-storage bucket create tfstate-bucket-g5el` +* `stackit object-storage credentials-group create --name terraform-state` +* `stackit object-storage credentials create --credentials-group-id CREDGROU-Pxxx-IDzz-aaaa-DUMMYbbbbbbb` + +* `terraform init --backend-config=./config.s3.tfbackend` + +Now we can start using Terraform. + +## Connect to created SKE cluster + +* `stackit ske cluster list` +* `stackit ske kubeconfig create scrumlr --login` +* `--login` ensures that authentication is performed with the stackit CLI and very short-lived credentials are used in the background, without this flag the credentials are static and and usually have a longer lifetime. If the credentials are obtained without the `--login` flag, they must be renewed manually. diff --git a/module6/tf/config.s3.tfbackend.example b/module6/tf/config.s3.tfbackend.example new file mode 100644 index 0000000..14e1476 --- /dev/null +++ b/module6/tf/config.s3.tfbackend.example @@ -0,0 +1,7 @@ +# Replace keys and bucket name here and rename this file to config.s3.tfbackend +secret_key = "xxx" +access_key = "yyy" +bucket = "tfstate-bucket-SUFFIX" + +# The key can be left as it is, but can also be customized as desired (before the terraform init) +key = "scrumlr.tfstate" diff --git a/module6/tf/dns.tf b/module6/tf/dns.tf new file mode 100644 index 0000000..813cac2 --- /dev/null +++ b/module6/tf/dns.tf @@ -0,0 +1,5 @@ +resource "stackit_dns_zone" "scrumlr" { + project_id = var.project_id + dns_name = var.dns_name + name = "Scrumlr Zone" +} diff --git a/module6/tf/main.tf b/module6/tf/main.tf new file mode 100644 index 0000000..5071f9f --- /dev/null +++ b/module6/tf/main.tf @@ -0,0 +1,36 @@ +terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "0.50.0" + } + } + backend "s3" { + + # Secrets and config outsourced to config.s3.tfbackend file, which is included in .gitignore + # See also: https://developer.hashicorp.com/terraform/language/backend#partial-configuration + # terraform init --backend-config=./config.s3.tfbackend + #bucket = "tfstate-bucket-SUFFIX" + #key = "scrumlr.tfstate" + #secret_key = "SECRETKEY" + #access_key = "ACCESSKEY" + + endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" + } + region = "eu01" + + # Also use remote locking + use_lockfile = true + + # AWS specific checks must be skipped as they do not work on STACKIT. + skip_credentials_validation = true + skip_region_validation = true + skip_s3_checksum = true + skip_requesting_account_id = true + } +} +provider "stackit" { + default_region = "eu01" + service_account_key_path = "sa_key.json" +} diff --git a/module6/tf/o11y.tf b/module6/tf/o11y.tf new file mode 100644 index 0000000..3de577d --- /dev/null +++ b/module6/tf/o11y.tf @@ -0,0 +1,32 @@ +resource "stackit_observability_instance" "scrumlr" { + project_id = var.project_id + name = "scrumlr" + # Observability-Starter-EU01 may be enough for a short trial, but the metric samples (per minute) limit is quickly reached with our example. + plan_name = "Observability-Basic-EU01" +} + +resource "stackit_observability_credential" "cluster" { + project_id = var.project_id + instance_id = stackit_observability_instance.scrumlr.instance_id +} + +output "o11y_grafana_url" { + value = stackit_observability_instance.scrumlr.grafana_url +} + +output "o11y_logs_push_url" { + value = stackit_observability_instance.scrumlr.logs_push_url +} + +output "o11y_metrics_push_url" { + value = stackit_observability_instance.scrumlr.metrics_push_url +} + +output "o11y_cluster_user" { + value = stackit_observability_credential.cluster.username +} + +output "o11y_cluster_password" { + value = stackit_observability_credential.cluster.password + sensitive = true +} diff --git a/module6/tf/postgres.tf b/module6/tf/postgres.tf new file mode 100644 index 0000000..4d00402 --- /dev/null +++ b/module6/tf/postgres.tf @@ -0,0 +1,42 @@ +resource "stackit_postgresflex_instance" "scrumlr" { + project_id = var.project_id + name = "scrumlr" + acl = stackit_ske_cluster.scrumlr.egress_address_ranges + backup_schedule = "00 00 * * *" + flavor = { + cpu = 2 + ram = 4 + } + replicas = 3 + storage = { + class = "premium-perf6-stackit" + size = 5 + } + version = 17 +} + +resource "stackit_postgresflex_user" "scrumlr" { + project_id = var.project_id + instance_id = stackit_postgresflex_instance.scrumlr.instance_id + username = "scrumlr" + roles = ["login", "createdb"] +} + +resource "stackit_postgresflex_database" "scrumlr" { + project_id = var.project_id + instance_id = stackit_postgresflex_instance.scrumlr.instance_id + owner = stackit_postgresflex_user.scrumlr.username + name = "scrumlr" +} + +output "postgres_dsn" { + value = format( + "postgres://%s:%s@%s:%d/%s", + stackit_postgresflex_user.scrumlr.username, + stackit_postgresflex_user.scrumlr.password, + stackit_postgresflex_user.scrumlr.host, + stackit_postgresflex_user.scrumlr.port, + stackit_postgresflex_database.scrumlr.name + ) + sensitive = true +} diff --git a/module6/tf/ske.tf b/module6/tf/ske.tf new file mode 100644 index 0000000..d2838da --- /dev/null +++ b/module6/tf/ske.tf @@ -0,0 +1,32 @@ +resource "stackit_ske_cluster" "scrumlr" { + project_id = var.project_id + name = "scrumlr" + kubernetes_version_min = "1.31.7" + node_pools = [ + { + name = "scrumlrpool" + machine_type = "c1.3" + os_name = "flatcar" + minimum = "3" + maximum = "3" + availability_zones = ["eu01-1", "eu01-2", "eu01-3"] + volume_type = "storage_premium_perf2" + } + ] + maintenance = { + enable_kubernetes_version_updates = true + enable_machine_image_version_updates = true + start = "01:00:00Z" + end = "02:00:00Z" + } + extensions = { + dns = { + enabled = true + zones = [stackit_dns_zone.scrumlr.dns_name] + } + argus = { + argus_instance_id = stackit_observability_instance.scrumlr.instance_id + enabled = true + } + } +} diff --git a/module6/tf/terraform.tfvars.example b/module6/tf/terraform.tfvars.example new file mode 100644 index 0000000..0beda82 --- /dev/null +++ b/module6/tf/terraform.tfvars.example @@ -0,0 +1,2 @@ +project_id = "PROJECTx-IDyy-zzzz-aaaa-DUMMYbbbbbbb" # CHANGE-ME +dns_name = "CHANGE-ME.stackit.gg" # CHANGE-ME diff --git a/module6/tf/variables.tf b/module6/tf/variables.tf new file mode 100644 index 0000000..1bbb60c --- /dev/null +++ b/module6/tf/variables.tf @@ -0,0 +1,9 @@ +variable "project_id" { + description = "The STACKIT project ID" + type = string +} + +variable "dns_name" { + description = "DNS name for generated Zone" + type = string +}