Compare commits
4 Commits
fix/drop-d
...
feature/dn
| Author | SHA1 | Date | |
|---|---|---|---|
| a99c7d8080 | |||
| ebe067088f | |||
| b0faaded6a | |||
| 7716d16572 |
@@ -47,14 +47,6 @@ resource "upcloud_kubernetes_node_group" "workers" {
|
|||||||
node_count = var.node_count
|
node_count = var.node_count
|
||||||
plan = var.node_plan
|
plan = var.node_plan
|
||||||
anti_affinity = var.node_count > 1
|
anti_affinity = var.node_count > 1
|
||||||
|
|
||||||
dynamic "cloud_native_plan" {
|
|
||||||
for_each = var.storage_size != null ? [1] : []
|
|
||||||
content {
|
|
||||||
storage_size = var.storage_size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
prefix = var.prefix
|
prefix = var.prefix
|
||||||
cluster = var.cluster_name
|
cluster = var.cluster_name
|
||||||
|
|||||||
@@ -38,12 +38,6 @@ variable "control_plane_ip_filter" {
|
|||||||
default = ["0.0.0.0/0"]
|
default = ["0.0.0.0/0"]
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "storage_size" {
|
|
||||||
description = "Storage size in GB for worker nodes (overrides plan default via cloud_native_plan block)"
|
|
||||||
type = number
|
|
||||||
default = null
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "tags" {
|
variable "tags" {
|
||||||
description = "Labels to apply to resources"
|
description = "Labels to apply to resources"
|
||||||
type = map(string)
|
type = map(string)
|
||||||
|
|||||||
@@ -1,72 +1,10 @@
|
|||||||
# =============================================================================
|
|
||||||
# UpCloud Workload Cluster
|
|
||||||
# =============================================================================
|
|
||||||
# A lean UCS cluster for running application workloads. No managed data
|
|
||||||
# services — those live on the platform cluster. ArgoCD (on the platform
|
|
||||||
# cluster) deploys apps to this cluster via the app-of-apps pattern.
|
|
||||||
#
|
|
||||||
# Platform components deployed by deploy-workload.sh:
|
|
||||||
# nginx-ingress, cert-manager, external-dns, external-secrets, alloy
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# tofu init && tofu plan && tofu apply
|
|
||||||
# ./sync-tofu-outputs.sh --env upcloud-workload
|
|
||||||
# ./deploy-workload.sh --env upcloud-workload
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
variable "prefix" {
|
|
||||||
description = "Prefix for resource names"
|
|
||||||
type = string
|
|
||||||
default = "clst-workload"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "zone" {
|
|
||||||
description = "UpCloud zone"
|
|
||||||
type = string
|
|
||||||
default = "no-svg1"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "node_plan" {
|
|
||||||
description = "UpCloud server plan for worker nodes"
|
|
||||||
type = string
|
|
||||||
default = "2xCPU-4GB"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "node_count" {
|
|
||||||
description = "Number of worker nodes"
|
|
||||||
type = number
|
|
||||||
default = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "network_cidr" {
|
|
||||||
description = "CIDR block for the private network"
|
|
||||||
type = string
|
|
||||||
default = "10.110.0.0/24"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "control_plane_ip_filter" {
|
|
||||||
description = "CIDRs allowed to access the K8s API"
|
|
||||||
type = list(string)
|
|
||||||
default = ["0.0.0.0/0"]
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "tags" {
|
|
||||||
description = "Labels to apply to resources"
|
|
||||||
type = map(string)
|
|
||||||
default = {
|
|
||||||
Environment = "workload"
|
|
||||||
ManagedBy = "tofu"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module "cluster" {
|
module "cluster" {
|
||||||
source = "../modules/cluster"
|
source = "../modules/cluster"
|
||||||
|
|
||||||
prefix = "clst-prod"
|
prefix = "clst"
|
||||||
zone = "no-svg1"
|
zone = "de-fra1"
|
||||||
node_plan = "CLOUDNATIVE-4xCPU-8GB"
|
node_plan = "4xCPU-8GB"
|
||||||
node_count = 4
|
node_count = 3
|
||||||
storage_size = 30
|
|
||||||
network_cidr = "10.100.0.0/24"
|
network_cidr = "10.100.0.0/24"
|
||||||
|
|
||||||
control_plane_ip_filter = ["0.0.0.0/0"] # TODO: restrict to known CIDRs
|
control_plane_ip_filter = ["0.0.0.0/0"] # TODO: restrict to known CIDRs
|
||||||
@@ -76,45 +14,3 @@ module "cluster" {
|
|||||||
ManagedBy = "tofu"
|
ManagedBy = "tofu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── Networking ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
resource "upcloud_router" "kubernetes" {
|
|
||||||
name = "${var.prefix}-workload-router"
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "upcloud_gateway" "kubernetes" {
|
|
||||||
name = "${var.prefix}-workload-gateway"
|
|
||||||
zone = var.zone
|
|
||||||
features = ["nat"]
|
|
||||||
router {
|
|
||||||
id = upcloud_router.kubernetes.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "upcloud_network" "kubernetes" {
|
|
||||||
name = "${var.prefix}-workload-network"
|
|
||||||
zone = var.zone
|
|
||||||
router = upcloud_router.kubernetes.id
|
|
||||||
|
|
||||||
ip_network {
|
|
||||||
address = var.network_cidr
|
|
||||||
dhcp = true
|
|
||||||
dhcp_default_route = true
|
|
||||||
family = "IPv4"
|
|
||||||
gateway = cidrhost(var.network_cidr, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
depends_on = [upcloud_gateway.kubernetes]
|
|
||||||
}
|
|
||||||
|
|
||||||
# ─── Kubernetes Cluster ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
resource "upcloud_kubernetes_cluster" "main-prod" {
|
|
||||||
name = "${var.prefix}-workload"
|
|
||||||
zone = var.zone
|
|
||||||
network = upcloud_network.kubernetes.id
|
|
||||||
control_plane_ip_filter = var.control_plane_ip_filter
|
|
||||||
|
|
||||||
private_node_groups = true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ variable "prefix" {
|
|||||||
variable "zone" {
|
variable "zone" {
|
||||||
description = "UpCloud zone"
|
description = "UpCloud zone"
|
||||||
type = string
|
type = string
|
||||||
default = "no-svg1"
|
default = "fi-hel1"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "node_plan" {
|
variable "node_plan" {
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-mcp
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "1"
|
|
||||||
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ""
|
|
||||||
notifications.argoproj.io/subscribe.on-sync-failed.slack: ""
|
|
||||||
notifications.argoproj.io/subscribe.on-degraded.slack: ""
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: forte-drop-mcp
|
|
||||||
app.kubernetes.io/part-of: apps
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: default
|
|
||||||
sources:
|
|
||||||
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
|
|
||||||
path: forteapp
|
|
||||||
targetRevision: HEAD
|
|
||||||
helm:
|
|
||||||
valueFiles:
|
|
||||||
- $values/forte-drop-mcp/values.yaml
|
|
||||||
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
|
|
||||||
targetRevision: HEAD
|
|
||||||
ref: values
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: forte-drop
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- forte-drop-mcp.yaml
|
|
||||||
# No keycloak-client config + no auth-oidc Secret for mcp mode. The chart's
|
|
||||||
# auth.type: mcp auto-registers the MCP client; the sidecar is an RFC 9728
|
|
||||||
# resource server that validates tokens (no client-secret of its own).
|
|
||||||
# forte-drop-secrets (shared with web) covers PG + S3 creds.
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# forte-drop Postgres — backup & restore runbook
|
|
||||||
|
|
||||||
## What gets backed up
|
|
||||||
|
|
||||||
A CronJob (`forte-drop-pg-backup`, namespace `forte-drop`) runs nightly at **02:00 UTC**:
|
|
||||||
|
|
||||||
1. `pg_dump` of the `drops` database → gzip.
|
|
||||||
2. Upload to **UpCloud Managed Object Storage**: `s3://drops/_pgbackups/forte-drop-<TS>.sql.gz`
|
|
||||||
(the `_pgbackups/` prefix is collision-proof: app slugs match `/^[a-z0-9][a-z0-9-]{0,62}$/`
|
|
||||||
and can never start with `_`).
|
|
||||||
3. Retention: dumps older than **30 days** are pruned.
|
|
||||||
|
|
||||||
S3 creds come from the `forte-drop-secrets` Secret (`S3_ENDPOINT` / `S3_KEY` / `S3_SECRET`).
|
|
||||||
Postgres creds from `forte-drop-pg-creds` (`pgusername` / `pgpassword`).
|
|
||||||
|
|
||||||
> **Object storage is the durable tier.** App data + DB backups both live in UpCloud
|
|
||||||
> Managed Object Storage (replicated by UpCloud). The in-cluster Postgres PVC is the
|
|
||||||
> live working copy; the nightly dump is the recovery point. The PVC carries
|
|
||||||
> `Prune=false,Delete=false` so ArgoCD never deletes it.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export KUBECONFIG=~/Downloads/dev-fd-no-svg1_kubeconfig.yaml
|
|
||||||
# Confirm the namespace + DB pod are up:
|
|
||||||
kubectl -n forte-drop get pods -l app.kubernetes.io/name=postgresql
|
|
||||||
```
|
|
||||||
|
|
||||||
## List available backups
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run an ephemeral mc pod with the app's S3 creds:
|
|
||||||
kubectl -n forte-drop run mc-list --rm -it --restart=Never \
|
|
||||||
--image=quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z \
|
|
||||||
--overrides='{"spec":{"containers":[{"name":"mc","image":"quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z","command":["sh","-c","mc alias set obj \"$S3_ENDPOINT\" \"$S3_KEY\" \"$S3_SECRET\" >/dev/null && mc ls obj/drops/_pgbackups/"],"envFrom":[{"secretRef":{"name":"forte-drop-secrets"}}]}]}}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manually trigger a backup (before risky changes)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl -n forte-drop create job --from=cronjob/forte-drop-pg-backup pg-backup-manual-$(date +%s)
|
|
||||||
# Watch:
|
|
||||||
kubectl -n forte-drop get jobs -l app.kubernetes.io/component=backup
|
|
||||||
kubectl -n forte-drop logs -l app.kubernetes.io/component=backup --tail=40
|
|
||||||
```
|
|
||||||
|
|
||||||
## Restore a dump
|
|
||||||
|
|
||||||
> **Destructive.** This overwrites the live `drops` database. Take a fresh manual
|
|
||||||
> backup first (above) and confirm with whoever owns the data before proceeding.
|
|
||||||
|
|
||||||
### 1. Pick the dump to restore
|
|
||||||
|
|
||||||
List backups (above), choose `forte-drop-<TS>.sql.gz`.
|
|
||||||
|
|
||||||
### 2. Run a restore pod that pulls the dump and pipes it into Postgres
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DUMP="forte-drop-20260530T020000Z.sql.gz" # <-- set to the chosen file
|
|
||||||
|
|
||||||
kubectl -n forte-drop run pg-restore --rm -it --restart=Never \
|
|
||||||
--image=postgres:16-alpine \
|
|
||||||
--overrides='{
|
|
||||||
"spec": {
|
|
||||||
"containers": [{
|
|
||||||
"name": "restore",
|
|
||||||
"image": "postgres:16-alpine",
|
|
||||||
"command": ["sh","-c","set -euo pipefail; \
|
|
||||||
apk add --no-cache curl >/dev/null; \
|
|
||||||
# download via mc is simpler — use a 2-step instead (see note). \
|
|
||||||
echo placeholder"],
|
|
||||||
"envFrom": [
|
|
||||||
{"secretRef":{"name":"forte-drop-pg-creds"}},
|
|
||||||
{"secretRef":{"name":"forte-drop-secrets"}}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Simpler 2-pod approach (recommended — avoids cramming mc + psql in one image):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DUMP="forte-drop-20260530T020000Z.sql.gz"
|
|
||||||
|
|
||||||
# (a) Download the dump from object storage to a local file:
|
|
||||||
kubectl -n forte-drop run mc-get --rm -it --restart=Never \
|
|
||||||
--image=quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z \
|
|
||||||
--overrides='{"spec":{"containers":[{"name":"mc","image":"quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z","command":["sh","-c","mc alias set obj \"$S3_ENDPOINT\" \"$S3_KEY\" \"$S3_SECRET\" >/dev/null && mc cat obj/drops/_pgbackups/'"$DUMP"'"],"envFrom":[{"secretRef":{"name":"forte-drop-secrets"}}]}]}}' \
|
|
||||||
> /tmp/$DUMP
|
|
||||||
|
|
||||||
# (b) Pipe it into the live Postgres via the service:
|
|
||||||
gunzip -c /tmp/$DUMP | kubectl -n forte-drop run pg-restore --rm -i --restart=Never \
|
|
||||||
--image=postgres:16-alpine \
|
|
||||||
--overrides='{"spec":{"containers":[{"name":"psql","image":"postgres:16-alpine","stdin":true,"command":["sh","-c","PGPASSWORD=\"$pgpassword\" psql -h forte-drop-postgresql.forte-drop.svc -U \"$pgusername\" -d drops"],"env":[{"name":"pgusername","valueFrom":{"secretKeyRef":{"name":"forte-drop-pg-creds","key":"pgusername"}}},{"name":"pgpassword","valueFrom":{"secretKeyRef":{"name":"forte-drop-pg-creds","key":"pgpassword"}}}]}]}}'
|
|
||||||
```
|
|
||||||
|
|
||||||
> The app's schema is created idempotently on boot (`CREATE TABLE IF NOT EXISTS` +
|
|
||||||
> `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` in `src/repo/pg.ts`), and `pg_dump`
|
|
||||||
> output includes the data. For a clean restore into a fresh DB this just works.
|
|
||||||
> To restore over an existing DB with conflicting rows, drop/recreate the `drops`
|
|
||||||
> database first (coordinate downtime — scale the web Deployment to 0 during the
|
|
||||||
> restore so the app isn't writing).
|
|
||||||
|
|
||||||
### 3. Verify
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl -n forte-drop run pg-check --rm -it --restart=Never \
|
|
||||||
--image=postgres:16-alpine \
|
|
||||||
--env="PGPASSWORD=$(kubectl -n forte-drop get secret forte-drop-pg-creds -o jsonpath='{.data.pgpassword}' | base64 -d)" \
|
|
||||||
--command -- psql -h forte-drop-postgresql.forte-drop.svc -U drops -d drops \
|
|
||||||
-c "SELECT count(*) AS drops FROM drops;" -c "SELECT count(*) AS view_hits FROM view_hits;"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Bring the app back
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# If you scaled web to 0 for the restore:
|
|
||||||
kubectl -n forte-drop scale deploy/forte-drop --replicas=2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Object data (uploaded drop files)
|
|
||||||
|
|
||||||
Drop files live in `s3://drops/<slug>/...` in the same managed bucket. They are
|
|
||||||
**not** part of the pg backup (the dump only holds metadata). Object storage is
|
|
||||||
UpCloud-managed/replicated, so no separate file backup is configured. If a
|
|
||||||
file-level backup is later required, mirror the bucket to a second bucket/region:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mc mirror --overwrite obj/drops/ backup-target/drops-mirror/
|
|
||||||
```
|
|
||||||
|
|
||||||
(Exclude `_pgbackups/` from the app-data mirror if you split them.)
|
|
||||||
|
|
||||||
## Disaster scenarios
|
|
||||||
|
|
||||||
| Scenario | Recovery |
|
|
||||||
|---|---|
|
|
||||||
| Postgres pod crash / reschedule | StatefulSet reattaches the PVC; ~1–2 min downtime; no data loss. |
|
|
||||||
| PVC lost / corrupted | Recreate StatefulSet, restore latest nightly dump (above). Data since last dump is lost. |
|
|
||||||
| Accidental `drops` table data loss | Restore latest dump; or `pg_restore` a single table from a dump. |
|
|
||||||
| Namespace deleted | PVC has `Prune=false,Delete=false`; recreate Applications, PVC re-binds, app recovers. Backups in object storage are independent. |
|
|
||||||
| Object storage bucket lost | UpCloud-managed (replicated). If the IAM key is rotated, update `forte-drop-secrets` (re-seal). |
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-postgresql
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "0"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: forte-drop-postgresql
|
|
||||||
app.kubernetes.io/part-of: apps
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: default
|
|
||||||
|
|
||||||
source:
|
|
||||||
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
|
|
||||||
targetRevision: HEAD
|
|
||||||
path: apps/overlays/upc-dev/forte-drop-postgresql/resources
|
|
||||||
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: forte-drop
|
|
||||||
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
allowEmpty: false
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- Validate=true
|
|
||||||
- ServerSideApply=true
|
|
||||||
|
|
||||||
ignoreDifferences:
|
|
||||||
- group: apps
|
|
||||||
kind: StatefulSet
|
|
||||||
jsonPointers:
|
|
||||||
- /spec/volumeClaimTemplates
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- forte-drop-postgresql.yaml
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: bitnami.com/v1alpha1
|
|
||||||
kind: SealedSecret
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-pg-creds
|
|
||||||
namespace: forte-drop
|
|
||||||
spec:
|
|
||||||
encryptedData:
|
|
||||||
pgpassword: AgBYokuQRCTmPGC8soB7n8W39nmSAZDTV97i77NmdMh5ndaZNtCtXxhMcpM2z8kaGv2tCWIh47dr38a5tGPZ0TsmeEQOF9Rbbtq1fyR7pJwT8S+N2Z2zu354wNWQlltEAVvTvthe2wTer/BpyRofeSZprxihmfNpuHUP8rLsnIXln5tOWDzJ8hnRWoYZFITaihC2qrJj/kFE0Rfdcmzt1tSq3jCB/rWijVaJF9XSh4rzQoqZDiUNjDPUjyERILw59JWU4zf9OKcqNHDnmpXBR4LSjLhd9waN6ElEzO4gGcVaHISKrTwewX1ONwPHDnw6lqkQObyBPx8aUsGzxLkUhNDtvIYDkB4BKWP5Qu4bNcSztIbrxi6l7Lr/DWC9qTbKm1/p83rc6r8VqRMURUyQg8/vBlCHOIUbZ8DM1OfNlMd8gvcSkaxEVIdCDUjguCvE3cGyG4cqv2unllZQ+9417WwLNJecT6x1EL3nQyAlK5c9vUIbcVyaFlbSUcGB7xmPgZrZ6/3RDyOH6Tmew1ssV9gLvdaehscUE0fjnnFnJpczkwdyxIOSNLIkjlWetCKEbhowJbzk05h3M2p6XQQOuNTnsYjAADMGD72GUgAQlY8KXmDELtv09KELcXbeYS4gABPpMrVmvZymq8lqQ13Py8o+cIqbrU5V86WxASTfQ5gMo/ymYabuhTBIapcnaKR1dFCCfu8deh5f2HJ6/1NjdWR+XvEshg+EF5OkTUInukX4vA==
|
|
||||||
pgusername: AgCs6vyQ8CIv5OneP/jMltIPGdZQbpq/BFmQM1mkBD61Ve+anzve5K0Gkg+zsNfbZf0pOPAXtu4C4aL1Lwv7gqpoe4Hp/UEb/X9uLfJ1b8ZitmM1XsPmmSiCskHjrc2BLkAvfrVIXkHc3LOY2uZ/E5stc6Ss2WFE8/uzzVXW0B8fdEK0criludQ8iwR1gypulEcDNomXgkK/1gmmCWosUcVv4jDMDhqBD+b9WYnBB6J73gUclWVMvYDFdNas2PuoRzu5Twc9TAZrTxN5lvLOXAonOo0YiUbUhEC83sfMWYDT5/9OxqcJhAxtgFe9j83MpCwLSwfeLZm7UsUapWDb60MxPJLGvoGD/ZOhkeYt/YCZYROa57TMslVIL5YU1KCiNWvtRjIqnvdiBxI7MRvPUfAoawS4ktT5PDhTTfrixFbaF95jul2kKBXV+OYB1UNsFhcCgZx9rzYRt4lNmBv4m4HeXIp3EYY8VlGLQ45BVVqjJ4QkISvb7ifQWH1aPMQllj+J3GwW0KJN0dEgsh1LT+C7W5I5mq461NOTF1eih/XRBeuPoLlgApxiGXvFCTx8lji2/JIdOaqcg29hdabSprxa0YMStChi2pbtHhRzAuFCp8mInGt8Q406vu67Y4/51yuwI40YeDVu0lf010TB+/v2Zy3OrNyjlqrD5JNynsLuRl3UhuAKC14Xhg/MiDLvTzfsYE8aog==
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-pg-creds
|
|
||||||
namespace: forte-drop
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- postgresql.yaml
|
|
||||||
- forte-drop-pg-creds-sealed.yaml
|
|
||||||
- pg-backup-cronjob.yaml
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# Nightly logical backup of the forte-drop Postgres → UpCloud Managed Object Storage.
|
|
||||||
# Dumps to s3://drops/_pgbackups/ (the `_` prefix is collision-proof: app slugs match
|
|
||||||
# /^[a-z0-9][a-z0-9-]{0,62}$/ and can never start with `_`). Retains 30 days.
|
|
||||||
#
|
|
||||||
# Pod shape: initContainer pg_dump → shared emptyDir → mc upload + retention prune.
|
|
||||||
# Both images pinned. S3 creds reuse forte-drop-secrets (the app's UpCloud user has
|
|
||||||
# s3:* on the drops bucket). PG creds from forte-drop-pg-creds.
|
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: CronJob
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-pg-backup
|
|
||||||
namespace: forte-drop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
app.kubernetes.io/component: backup
|
|
||||||
spec:
|
|
||||||
schedule: "0 2 * * *" # 02:00 UTC daily
|
|
||||||
concurrencyPolicy: Forbid
|
|
||||||
successfulJobsHistoryLimit: 3
|
|
||||||
failedJobsHistoryLimit: 3
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
backoffLimit: 2
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
app.kubernetes.io/component: backup
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 65532
|
|
||||||
fsGroup: 65532
|
|
||||||
volumes:
|
|
||||||
- name: work
|
|
||||||
emptyDir: {}
|
|
||||||
initContainers:
|
|
||||||
- name: dump
|
|
||||||
image: postgres:16-alpine
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
set -euo pipefail
|
|
||||||
TS=$(date -u +%Y%m%dT%H%M%SZ)
|
|
||||||
echo "dumping to /work/forte-drop-${TS}.sql.gz"
|
|
||||||
PGPASSWORD="$PGPASSWORD" pg_dump \
|
|
||||||
-h forte-drop-postgresql.forte-drop.svc \
|
|
||||||
-p 5432 -U "$PGUSER" -d drops \
|
|
||||||
--no-owner --no-privileges \
|
|
||||||
| gzip -9 > "/work/forte-drop-${TS}.sql.gz"
|
|
||||||
echo "dump complete: $(ls -lh /work/)"
|
|
||||||
env:
|
|
||||||
- name: PGUSER
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef: { name: forte-drop-pg-creds, key: pgusername }
|
|
||||||
- name: PGPASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef: { name: forte-drop-pg-creds, key: pgpassword }
|
|
||||||
volumeMounts:
|
|
||||||
- name: work
|
|
||||||
mountPath: /work
|
|
||||||
containers:
|
|
||||||
- name: upload
|
|
||||||
image: quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
set -euo pipefail
|
|
||||||
mc alias set obj "$S3_ENDPOINT" "$S3_KEY" "$S3_SECRET"
|
|
||||||
mc cp /work/*.sql.gz "obj/${S3_BUCKET}/_pgbackups/"
|
|
||||||
echo "uploaded. pruning backups older than 30d:"
|
|
||||||
mc rm --recursive --force --older-than 30d "obj/${S3_BUCKET}/_pgbackups/" || true
|
|
||||||
echo "backup retention pass complete"
|
|
||||||
env:
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef: { name: forte-drop-secrets, key: S3_ENDPOINT }
|
|
||||||
- name: S3_BUCKET
|
|
||||||
value: "drops"
|
|
||||||
- name: S3_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef: { name: forte-drop-secrets, key: S3_KEY }
|
|
||||||
- name: S3_SECRET
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef: { name: forte-drop-secrets, key: S3_SECRET }
|
|
||||||
volumeMounts:
|
|
||||||
- name: work
|
|
||||||
mountPath: /work
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-postgresql
|
|
||||||
namespace: forte-drop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
app.kubernetes.io/component: database
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- name: tcp-postgresql
|
|
||||||
port: 5432
|
|
||||||
targetPort: tcp-postgresql
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-postgresql
|
|
||||||
namespace: forte-drop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
app.kubernetes.io/component: database
|
|
||||||
spec:
|
|
||||||
serviceName: forte-drop-postgresql
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
app.kubernetes.io/component: database
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: postgresql
|
|
||||||
image: postgres:16-alpine
|
|
||||||
# NOTE: no securityContext. The official postgres image's entrypoint must
|
|
||||||
# start as root to chown a fresh /var/lib/postgresql/data, then drops to
|
|
||||||
# the postgres user (uid 70 in alpine) via gosu. Forcing runAsNonRoot here
|
|
||||||
# breaks initdb on a fresh PVC. Matches the vaultwarden-postgresql pattern.
|
|
||||||
ports:
|
|
||||||
- name: tcp-postgresql
|
|
||||||
containerPort: 5432
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: forte-drop-pg-creds
|
|
||||||
key: pgusername
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: forte-drop-pg-creds
|
|
||||||
key: pgpassword
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
value: drops
|
|
||||||
- name: PGDATA
|
|
||||||
value: /var/lib/postgresql/data/pgdata
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /var/lib/postgresql/data
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- pg_isready -U "$POSTGRES_USER" -d drops
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 10
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- pg_isready -U "$POSTGRES_USER" -d drops
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 256Mi
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- metadata:
|
|
||||||
name: data
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-options: Prune=false,Delete=false
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
storageClassName: upcloud-block-storage-maxiops
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Keep at least 1 web pod up during voluntary disruptions (node drain, upgrade).
|
|
||||||
# Pairs with replicaCount: 2 so a drain can evict one pod while the other serves.
|
|
||||||
#
|
|
||||||
# Selector verified against live forteapp-chart deployments (mcp10x, argocd-mcp):
|
|
||||||
# the chart's pod selector is {app.kubernetes.io/instance, app.kubernetes.io/name,
|
|
||||||
# component: app} where instance/name == the ArgoCD Application (Helm release) name.
|
|
||||||
# Using all three labels also disambiguates the web pods from the forte-drop-mcp
|
|
||||||
# deployment that shares the forte-drop namespace (its instance/name == forte-drop-mcp).
|
|
||||||
apiVersion: policy/v1
|
|
||||||
kind: PodDisruptionBudget
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-web
|
|
||||||
namespace: forte-drop
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: forte-drop
|
|
||||||
app.kubernetes.io/part-of: apps
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
spec:
|
|
||||||
minAvailable: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/instance: forte-drop
|
|
||||||
app.kubernetes.io/name: forte-drop
|
|
||||||
component: app
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: bitnami.com/v1alpha1
|
|
||||||
kind: SealedSecret
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-secrets
|
|
||||||
namespace: forte-drop
|
|
||||||
spec:
|
|
||||||
encryptedData:
|
|
||||||
BASE_DOMAIN: AgAFybdBryVb2AQuGQC8REXzW0YZlyycJp/KeXnROkW71UjDe4qMAWkWszrJWxZMvAPO/tXmibp7jEol6aB5GKG0k3tswWoprTFXLd9CMR2U9SWR3ZCol4npPXo7uOxhBcNSVt+cDXyejSiFTi6goY2oOtbKAJSF9Nv7Z5ePaqhhFni3ntcmM0S1Ad1l3QR7VvyazHFBXfO0b8Z9NgYsUNbGrXWDwoSAZIv3ly3wx90AXn+dXX5FNPtl9CtyAVhHsl3liwQdhEwS2krZZjj7NiQTCfNXp7BSB9ZETpo9KkoV4AZNy1zupd3HpeXHsyhHjq/JqXIAF3iFU0tZTWjhcwnehYdEU5oduwfLCWym5PYgpiQAGiazpkm1Ss3/PYpZYnR2nWv60b1Pa5i79ZiPNi4GL67AiWoJDw6QxV0Kbzi0AvUkZI1E2PeIJvv1w9NKdMRo49xK8LUx2qSTpWeqRP+1kzklHqclTuNVxiWtR2wUgdoLzvU7p5ETu7kPEmaoE8rYw4dKgQvHlMok2Ky2JsELGBkCiYjUN75T+yNlGs5dzbiwtWOja/r0dJ3ZGBQjcK4/BbTLiMYsrxmJTPPF/2zhCOlFY6cfcRMmc7Mwr68mK9m2rTOJQNjBMDoASiqVMmeSqfRSln7JNb1pAeq4xcz9YJMBJhPy2XNiBvRJK3pGIjVcNST0jSpic1X01NJTy7aFbcniZzYnsKJV61AQb+daGEsB1Ib3GnJ+Rv8+9NfvWg==
|
|
||||||
PASSWORD_GATE_SECRET: AgDIiAPb7bCpTeORoZXIn7Is/yKT7Qm+qSexgmXuXpe+WaLCJDWM5uOlvxp5tbtK2KD3SNtUDrOJQYfWYBOAPrBMBnSpDr8Ie7/wlJBxXCfROk1TldOU3s5O+5OnFfTDS9rAdcWdZdJ+Bt0aktnpuHJpjdurFca5o8ne5SRwYtGk6mNinCYRcnwMApZ9Y7IxvC9xzOi1QIhoOepMrotRkVJtbrXiclN5cI+P/nU8LaGwM4AEJtO3L8zY5QK30SZ/Tsz20Qo8HHGdIBLhHbhTALrFe8+n+Egda66Vr1DkKgDERW59PeWBd6xHCRGNuAk2ZzUmuiN5DJmWgacAGy9QblMMjhOIGKgiqsd4jx/8wC8FoyxBSstGFajahL2C6oFzCpbp5NiS185znN0ohJXqQzkp5RbcBliSYUZgHC8D1jDGxzcTksD2Hgh2NIAlJOStsfB3fUb6hWIFzxim0lyP9kFM5y92Sf5B1/h6PeHKOi1CMC5RFAEKhoyM4W1W+NxASJeT/NptTehcrjBPxLVKBPh/qgMcuNYNk34EB0asELalYBJ5cxzt31LLiLE/uDxCydXiSk7ACho7LxNffcUUakEwwsJDmjFiCscu83TfZq2vw5/2bgOzUng4XGDBgYwwr/KEHueGX4Keg255R3KqxyiMSKDi4CUEtYShjSsSYwU62rdogOQu20N3HGVn+/CksqKky7GoD958d4PwT5MGJwQhp8EjemBomDxTKNsO+C4moMNZpAEBtPlP65Lc1cfcaLlaFrwP4VtYHLXXBIvMzCC+
|
|
||||||
PGDATABASE: AgCCkY7AUaoy2V+fRXrEeLODlHOTXWXmWCW6MBWEY1J32QrN0bWUmwDdTxv5KHchbhH8lra9bABNOGfRcz59GIg8fH3fwTWOE8luh9cD50QkphjZPe9eGXc6YzheG0CG5hDnUoiJjnP9/l5cZc6sRxAo7for/bbLa0zja3VMzI+NMhVVRZT01G5R/Aoyf+B/TGm4mYbMyWAIgEEfjl50yIkc4WscfQrRkbxAtBF2qFuS3OL95TYSRULBAt0f3Z2WpCdS2b/pUyHJuuoi8aTo1a9QLFMrUdxVi6ydIKaMcx8xR8DlXQLOOdtIQu4TrgzGX2MgHwrbWuf/SFO5IWB/6/JI3yubo+i88M9LwPKsGeslcquoBG3Ibqlnmw8pcPrXUwBq2BowjAqpGsxdiR5XIi2j8QnpA8dTmFARX9CWKHoXFH5+uxx7SSTPG+izlGtVspsu5xx3F8MZ6eStInCyBimTVrn74+IvMPEBrj7wThO6Bl9EA3POkTRf6AmjuPItTgZK8lfXY/t0qDN5zApeB051+jSGZ0/o4PaY414vCm/+OteuCAI4ooCpDOwG3QD52VtpmoAvGjtuWExrvCHMW+hTIxsNbYJ+2SE1oQy8n8J5kKK+AU/SnYf/VEpbawHrpU48g6sDUHQzzPhoLN41j67qopmfRZAo1r5Tb4n7MnEOF3Yi7aT3lE6JvB3gZUSRqnDYSWObTg==
|
|
||||||
PGHOST: AgCWDPCQ4P+vPKQXmzNspODUJYVidwQDzFRxY8JDj5JYW1u+2xK8LqE2uWWYCW3YjF+H6yJoH6gu35KqOtjm0E23zMlknwZwkK+L93J/nrnNZHCznoub9jEzaAsTRhytvuIUBPQnFulP/t9Q04RjmR9c14puS7VS/tM57/cLq5zq/BiSYL3Wx2lblE7Xn6k44+8robPP3YkmlOuqziM1li/qevB/jxRV9YFjIpCz6D7WG6Kkx9CA6fzgalMJ5ErUVTJOuLDDty3qKd+9mg4WCLahjyzhPq/rIPUFE77EBDCulnnlMyvOSPSP7pMzkaNR1Ce3cOxOYAZ5mO2u5Y5pXt34V8iPe87N85KHQNZTu1Mu5ELMLdhEYOlO5ZX2x/RsdEuriWz1NW8MC57pEUGE/XrFhDe6x80eHi++qDRat2d04rTBOtlceZMmPVxs8tTa3VaD4PepekSogddLVooscLL7lYIpyR0Q6qOA1NdF2uOpx7hf6QLY1HeD4V8RSVVxV9uK5lUTrnBjtU/aUwnGs0xKxVNdBEKwG5Kh7qdsgyBiEa9V4Y2ElteGHave4J7xzimdASrbgMnlgIfcMuo94eX/0M0BAmpD4bEByMCWbcFgAfNnIOpXLZvoEfZ3DxVysmCzD+0nq+CEatdOjKp11EKbtf3H4PgnKqWDe2p8FNPXhev60CMhFvXS/QYg9fe2YJt/wqcN5MstCej4IuiOJkuFJ0UhZ/F5760xhuPYCUWP1suIVZM=
|
|
||||||
PGPASSWORD: AgBH32G+EUtc3jzGCA9bf27TCbzgK9xz+r4dqd0QQJL9xHbqgOARGVVaQ88AOkWV5VgYjqc/GFp51jLzVOHxgLdkqO/oCBuX9ajQEoGfq24AxFnaB7fh+Vlc3/N9yhT8lWoxHmHjyMVeX75g/9KvhNaRKBgiWQNHlt1C2FNh1h3U/aMfWVJIENmKKH2A5sxWe5haB7nynZc9r1QXBQKa7XVpuxAFXDHz3j3cFyR5Qflp+ac2APEM1/xbiaZDgkBtBd6dsDoCP56Dr1m91kaRGgbeX6WmRJ/Y89WAp4yt3QVfa8uGL1+DrBBMcfB1nAQKA45eZjPE6zTOEHxgTETCcmXJQiOzttDmBHRkIClOLLipgGDJwMqtgQoEMoJKjXMC0rsRy0NVRmibZa310R3PQjuHrQXxRD9ZAXkYg3opwLKeKi07b/7mvLHr7hU81fkBGnqNm/6heOSAqDZfRdregbBbcI/go72aypn2vQ5R+ozCdfwcp1tGla8FGpkI+zAdBKihp5Yo21VZ83FlIMq2JHF2+tv58C+LFeyqL1nr6BUmGKUQ+lEOnRzGYo1sbO5wBChc6yP3ZbzZYfxfvXAdfDY7vZUsareOC4uyR1wDnIiJgQ4kqmAKf7HulJJatKNgsvbmukj7c6lHLsfFRg5pwLO6iese9TZgtima2wkdcRpHSdt4ycnyHbwrEZ4kepfFlN1pGUl573/3l2cOdzO+WLCqV96P5myL6OOmCTxOaLdSyA==
|
|
||||||
PGPORT: AgCXyGjhlTcq3ffD+ZZZxe5gvqRLQl2MYGl7SfJDjw4LMdr3iojAeQNdYZIeZN4Bec9thhz1DI/Bc5Iiw5TiB3MAAvl/XtQM/T23JrSlUsfqECuHKQGw5kkgeVCds4j4d/IHFt9yBv4W4+ogcL5TZIGGgMiKa/99T77xgE4OqjeqqC+bpFuLObiKt5sYcqcHl2DDshTy4xeukfdA2yI3N9Tq7zfbLpZUKHWWe1gE+FbA0s+QA1ZCYv9uEtu/E5BWOYxE0u4GKhMDfMpguks0CNnIoRpovY3vmOxzFtqa7RpPoQrQtEN2Xtizu/G8K8p+mZLQ0cf3KX0rwp8tAsVKWrp8FYOy6+2OLYlhmDl1hK+M15czaL60DVBqiRIlQFdj9K+s5KQ0qfsG9m4v2WPSeCLEKwcsyLyq9vzXbDKwcMN2DOMOan8VDo04UZzkfdZa3lztfw7MWIdpvB0+cpgK3mdbbPCqSIQ8UTjeSVBgEz4EPUds81W8gqAgmeVKpUQplgFuLNwCTnCkuMSNiA93D3dgGccXzXiDrIBIcLzxHAWr0XEjISyD/pmNEowbDvXB5yRlNi1ZB5AHsRPurUYs4XtZN5ar+sxyex78tG18OkcBbABatBp3ol28Rs1LEN+HJupzNl4XDUx35/j3t/FPcw9PkcF+hzaQL7aWj4EDEWfjlzz5FEBlU5N6sd078tB79TpasuIk
|
|
||||||
PGUSER: AgDH9oSzu61NVOUlK7Evr278VuOx7ZxZgdsk+kdJmwpBGX7r5gmPrdomh/mqLYTuLokkanFiTfchRWHc76FvjbA/KxqwCq1qsZbW+dXrRtx/z1wQApKxNUJ7JolwMwP7tHE6QlGzO2mWj6RUROnhKpNybJXVvC3E5sSyz2QWC9hjamQP997RGA9yiiT/OShC7I6drFYR5cRDtpjW7Sy46qhMwlCRppiKh3wOV7qIAa0aPQE3Rfcg2WpK2ugRL1N+SiVnM+wPQwYVLiDaVF40vP40Kari99hIgmhcbjPeGG3kGX5VLww9KGm7iryrW3Yx45L/CCh1arUUpjkK2FGLVKtb3+YmDadnOA/I8Rr5kebhoMc93E3U3+mDfQA3cO/23xgpOJEGRCQwBlN9mazqkdq4zQkb4+nuxsdyQcxYtncgxhfCcZ0mXnbX2aW2kYcxKqa/jNjBcEpGMvos7dq6QzNq2nHrITo15S74M0292CAje2NFvKURA/KZnT26dDw3e5xa74E1nI/tBJEHWrUwRXpPu7naCZ2sZMxQV6ixQMuDakx3YamXZmMwgFO2FZ6ZL9BDDsbV4+JAsNwEaHGIIaTbE28R/xPIcUqcxrQV4ZWmHnJXFGyJ0XXxJ57GGjs7QwvvzAm+9WGYtlSC6H/8rX8uZIQLr3llVbJMuLpIv45i3p0Nkx8jyxGSG3rNQ4l3K0rjly2qZg==
|
|
||||||
S3_BUCKET: AgBcx2UvWafkVQ4fc+Xuc+gCLm5O5DceYSYBslL1bd8p4hKI/2hJF9z9UoLyAedyyrx8pHizcugkcIccrV1iyfQod2lDrivVJCy3qHz0mrQ6ZCOAk9ApNJTYUn+x/AYZL9EKVj/vfEzSwOTqlri6H53aUrZr8lv01TqLcrZDeG+jv64psydYKmSESkPl0iQ9DOx2sGkowNYbiTjux+ED9yXDMDdzojhMepbuUHhjNr7V7Z2NpX8+pNVz2o8o/oIA0zAJs3+C02018N3cyJ9P9BP2/qR6N31aykyI4P6GU5WlL0CDGKaheYy9ukM5x8SoU87yZBHLUxN0MhrMVKoFswU6CwTG/A8Gjkp84ce2cvlnuXP6JEMlqgSb+O+SeCH8+kwL9A87xmb42OOXSGokS188/2/13b6S733WL81B0RX1X1pOfWybpJzDhgiOC3Pjn18ItVHjKw3FlY6kwk1+P1vbZoRl343avRdyQyJam28A9UvZeTG+ac7drRTDndKYKGP8IWg/A32MEFSiKHsvnRQoYE7KORyzJxp610L1+w46KOUF5VF2afch02Jt4uLcmCOyUb6LdcWln3Jiz+wydJI1HK1RPio5oUT5d5se3K3DCa+GffbPMorHe/SdzqaBH2+uBcQn+wZO7JC1ei7hz4U1xdV41gkG1uq7Xz49ymW6P7G6qUr65MtvD73iMRlnJt/mdkw5UQ==
|
|
||||||
S3_ENDPOINT: AgDA5n96LeW1spJfmm7f9lcus5Ndv/91HAlhOfv0YwP+9R8rQhKF1sEL9Oi7Fpd4VJCQi9vDbeWYjGOMtsMqsLJivcZK6367rdSqL/YpKax9SkDgU+UG5Bpr5SDv4p5C3/J4+GPkZV+BduJQlNVPycTrdM80VBK8aALCNs7gGxy68v6mMgI90MpTp84ZeWgtJ7ZpTySivpwgycwcezldJTYDDjOvVfbK79trsRzDtjbAZzbDn0M8At4LYAfY7JTqcdCRHSGnzyg+81CX2jjyMOH0K55SsIqPEBdXqvh+VmC+jPyFMzfAN5hA8+otWTXfI7VucxAMZtRUiLU3I4/kIO60OkVjEf/SWrJv3nKb85aVfvRZ9k/A7kJ/ysKTXz2/iSAHU1DHTJlMW7SfhXciotx1p3BWOREF0QAQvrNCURFP+eZogBCIuiP38fD0gHkhG2BZkAY/OVZTr17a3F02kwemAuJ85hgUf0iwFV+/LD6X2OiqM85lnac/3B5Qp7UQf+Enn5RTqd90Pk/7QtAf/zTDmfxiWTkwpKXeQ62xcnHL9dQnUmDDs16eJZd04AJGgT8yrC//2lVOht7u6KuyG/+JXRxLBXAkGkr68lHBBuGmeVZ/t7ChL2Asyfu/uUTl9E1BOUfNvZXxwyLgqueL0Uw/MGLgswBsP6pYNGraMF+K5v7dse8cUxpyqk3s1C1zqoAhkehsCBtTjI+hF90G9Nh6mVSocoH12znt7fgUnwNbpg==
|
|
||||||
S3_KEY: AgAiciTM2ZNVlU1M1CNNXLkhCEYYbO7q5+Mp/DoC4OHBgIDKVfHurH39Dniuwxe6DcvE3vG2glRyTxQEg/ASLcwa7HBwBAwXe3wl1tRGM5Bp40/Vq935BXpkhcdp2fSoup8lPEbKS8q+L5LOqUlx7jmnkHXbI1tasz63KE8O9RUFDdQ8Gxy3nn/u4xkvibYxwmo60ApLKYgOu/ODPEETrWBcITHAVFUxbA8Kr9X9mPm3VpfrnFcUlxsCFr/zZwE/Y01eWdi8GGafb+apDPKMd7mAsLHFcPIQlpkHVT1M21qwwntZg9yV0RBACNu5BVPUgbmtUOQeWYMXn3FE+NJ7ajfdKAUCcEUV/f4s00b0S7jJTJwOUixDquMKSfu00AwDRCs8UcouikZe110uWnfEF3tVE0xQGF/3ItLni9VugBz7wQv7ACvmwnHmX5ZcjE0hxYcIS7ABWgHOZxgWoRWPao8eNAATipafcVIG1szl5ZMNTmAqHFyp2dlNU3zaiW6fz4q4CU7SrlhsrtqYM788qHvpJvDpFdF/i6oitH9CgpwmdCpH6YbBXxatnkWq9bqjEFcSZGDfDyT+iZaoPwhiOfaEoCyKlZ9RLLaK3E8zFcCDRXHnvnkqPtP/+VG30xz9pIat2EVB1N4b/kVIrr+fIM28mwk0vkC/tU8T55GF5BZr7VaYedHM9DVcQ4OJl7Ctrc9Ki8PXrne8gywyomA0F+YY9lxdDw==
|
|
||||||
S3_REGION: AgAAHTbNQ3gGnvg67ck2N5zSKKhMwR1j6pi/tZzYMEPK8jSnDTBkLYAt6ZVRtdsO+dG9kjnMsc/xTUMxJspbvQkgLSd9mG5FzJ37rBn+azCCSTDYBKq2ddGK1Yf/9w7MxOgN8aCyD4QCFOzR0EI49GbVYQynxDD5BwYuf7y4t2xCYt5wRGsjyNAmH3202Z90XKUkts8Na1hyD+xrrtrAtNfugyZqKo2WUSsHu82TD+cu5xZI51oQ9w9Mh9LaH3nfn/X5S+t17TjYvI7/c6hOwCVv10OdoaZa+SzqOvy7XxnqAShAYkPqJKfWhjecE1b9c/Cun32X4MRI0GBWyA02z4nR+WBbaVmasicx6hchVn8/wZeKMIl68F5LE7184MKKPNNwqsYslwFhuWq8dEw3TaVvnWgx3+cSMiX15SBwcLtE2UzJp+jjN/dpQ2MM0+6uV1GK4mNso5JAwGpUUUi2i+V1Ng7uXipI+5J9w6K0IMg1puGqFyahby42saSuH6vuPnjSx+2dXQTlgbl2SvCBoCgyOOJs4q5IafEvupAmRCNzx2HHv8/z6CFbSQZITQ3plmyNGLFXjynVw/6Q1PD4z3Pows4uVcYOEPbC0UoaXVgwgdBWa2N44BUhpdbqJUCyKuapLigpjKujG43jmdLzuk6gaPeH7SJTZKr624vs9hrGhzQYKdl6FZOQhmCFRDKXVCUiH2Z2pfwd3oo=
|
|
||||||
S3_SECRET: AgCcVQ7YtBAGpKBm+rE/hQBHrFlX5O0JO94xkZeoAppA9Tf8YR/PguZRGBWgLdNEJRI8C08lRRCUX3PY68jTySyjamb32iQkslOXGjAfnULeNGoGg05nLY2ZDYCEom6ieL8cc2xfbrV3yHoPQ7yVz9vcLjh1vATxyfdkqMapl8FpvQf0k0Zecmw3rLWE9y6vAn6Gb+/CWTnuhcW/8uDykmjIBTDQQddWshaZi+HosHyDbNxlnGj4U8mie68wytpS+Unp1gIWWE0hvelqO/3OUEEBB1OYMLV2DW8v86HXAE1Ix9jiCpSbyB+UzjOlrE/p4fJpeG4FtUC+/5ibRSxxgQRQYklKFJmdRDYWUnOngjgcT/Ewe41mTrpCUvb+jtir68pYLmVrLoha7S60w1YQHNkDAN2GftOyBjkkt6MtUDNzvNkfnKqKGUWyDSC27yfJdE/9k/4lDxQs0Sp20kIuz66/culBpg/s/oPSNs4SolCqG3GVLlKL775uqwLLuDN3txlPLb+Ex5vZAUapke+rn2zXzJVc1qlPfI/96vSEy6cx58LXdBadmBXn6c4Uy2MDa66EwsxOMXxzGLTd7AGkd5oeQVYfVPdTfGV5zx1AdzQhP3u/DD5FhKeWGDOr21iYB2jNm/P/hw0nFP2pf83W4/jLzPvuth1LF/WLF8cjclnGbcep2Kxrh/Xq0LmufofuVJyEI9/fl6onl5KIa6ZnVBJ8TsQesXJtNEKt9cPHiCvBKfLj5C+a4FlY
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
name: forte-drop-secrets
|
|
||||||
namespace: forte-drop
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: forte-drop
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "1"
|
|
||||||
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ""
|
|
||||||
notifications.argoproj.io/subscribe.on-sync-failed.slack: ""
|
|
||||||
notifications.argoproj.io/subscribe.on-degraded.slack: ""
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: forte-drop
|
|
||||||
app.kubernetes.io/part-of: apps
|
|
||||||
app.kubernetes.io/managed-by: argocd
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: default
|
|
||||||
sources:
|
|
||||||
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
|
|
||||||
path: forteapp
|
|
||||||
targetRevision: HEAD
|
|
||||||
helm:
|
|
||||||
valueFiles:
|
|
||||||
- $values/forte-drop/values.yaml
|
|
||||||
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
|
|
||||||
targetRevision: HEAD
|
|
||||||
ref: values
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: forte-drop
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- forte-drop.yaml
|
|
||||||
- forte-drop-pdb.yaml
|
|
||||||
- forte-drop-secrets-sealed.yaml
|
|
||||||
@@ -2,12 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
|||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- ../../base
|
- ../../base
|
||||||
- forte-drop-postgresql
|
- dbunk-demo
|
||||||
- forte-drop
|
|
||||||
- forte-drop-mcp
|
|
||||||
|
|
||||||
# No patches needed — base apps already default to "upc-dev" value paths
|
# No patches needed — base already has "upc-dev" paths
|
||||||
# (upc-dev is the default/base cluster).
|
# upc-dev is the default/base cluster
|
||||||
# forte-drop (postgres + web + mcp) and dbunk-demo are upc-dev-only apps — their
|
|
||||||
# values hardcode upc-dev hosts (drop.forteapps.net etc.) and must not sync to
|
|
||||||
# upc-prod, so they live here in the overlay rather than in apps/base/.
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ spec:
|
|||||||
resourceGroupName: forteapps-domain
|
resourceGroupName: forteapps-domain
|
||||||
hostedZoneName: forteapps.net
|
hostedZoneName: forteapps.net
|
||||||
environment: AzurePublicCloud
|
environment: AzurePublicCloud
|
||||||
tenantID: 063afd9e-5fcb-48d2-a769-ca31b0f5b443
|
|
||||||
clientID: 3b7a4ebf-894c-4f5d-9b1e-2b61312f8e74
|
clientID: 3b7a4ebf-894c-4f5d-9b1e-2b61312f8e74
|
||||||
clientSecretSecretRef:
|
clientSecretSecretRef:
|
||||||
name: azuredns-config
|
name: azuredns-config
|
||||||
@@ -52,7 +51,6 @@ spec:
|
|||||||
resourceGroupName: forteapps-domain
|
resourceGroupName: forteapps-domain
|
||||||
hostedZoneName: forteapps.net
|
hostedZoneName: forteapps.net
|
||||||
environment: AzurePublicCloud
|
environment: AzurePublicCloud
|
||||||
tenantID: 063afd9e-5fcb-48d2-a769-ca31b0f5b443
|
|
||||||
clientID: 3b7a4ebf-894c-4f5d-9b1e-2b61312f8e74
|
clientID: 3b7a4ebf-894c-4f5d-9b1e-2b61312f8e74
|
||||||
clientSecretSecretRef:
|
clientSecretSecretRef:
|
||||||
name: azuredns-config
|
name: azuredns-config
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ metadata:
|
|||||||
policies.kyverno.io/severity: medium
|
policies.kyverno.io/severity: medium
|
||||||
policies.kyverno.io/subject: Pod
|
policies.kyverno.io/subject: Pod
|
||||||
policies.kyverno.io/description: >-
|
policies.kyverno.io/description: >-
|
||||||
Injects an auth sidecar container into Pods annotated with policies.forteapps.io/auth: "true". Supports three auth modes controlled by the policies.forteapps.io/auth-type annotation: "token" (default), "oidc", and "mcp". In token mode the sidecar reads credentials from a mounted Secret volume. In OIDC mode the sidecar uses OpenID Connect with authority and client-id provided via required annotations (policies.forteapps.io/auth-oidc-authority and policies.forteapps.io/auth-oidc-client-id) and secrets from an auth-oidc Secret. In MCP mode the sidecar implements OAuth 2.0 for MCP servers per RFC 9728 (Protected Resource Metadata); Dynamic Client Registration (RFC 7591) is handled natively by Keycloak and consumed directly by MCP clients. Configured via policies.forteapps.io/auth-mcp-resource and policies.forteapps.io/auth-mcp-authority annotations. The sidecar port defaults to 9001 and can be overridden via the policies.forteapps.io/auth-port annotation. A NetworkPolicy is generated to restrict ingress to the sidecar port only.
|
Injects an auth sidecar container into Pods annotated with policies.forteapps.io/auth: "true". Supports three auth modes controlled by the policies.forteapps.io/auth-type annotation: "token" (default), "oidc", and "mcp". In token mode the sidecar reads credentials from a mounted Secret volume. In OIDC mode the sidecar uses OpenID Connect with authority and client-id provided via required annotations (policies.forteapps.io/auth-oidc-authority and policies.forteapps.io/auth-oidc-client-id) and secrets from an auth-oidc Secret. In MCP mode the sidecar implements OAuth 2.0 for MCP servers per RFC 9728 (Protected Resource Metadata) and RFC 7591 (Dynamic Client Registration), configured via policies.forteapps.io/auth-mcp-resource and policies.forteapps.io/auth-mcp-authority annotations. The sidecar port defaults to 9001 and can be overridden via the policies.forteapps.io/auth-port annotation. A NetworkPolicy is generated to restrict ingress to the sidecar port only.
|
||||||
spec:
|
spec:
|
||||||
background: false
|
background: false
|
||||||
rules:
|
rules:
|
||||||
|
|||||||
@@ -19,9 +19,7 @@
|
|||||||
"dotnet-sdk@latest",
|
"dotnet-sdk@latest",
|
||||||
"opentofu@1.11.6",
|
"opentofu@1.11.6",
|
||||||
"_1password@latest",
|
"_1password@latest",
|
||||||
"github-cli@latest",
|
"github-cli@latest"
|
||||||
"upcloud-cli@3.29.0",
|
|
||||||
"awscli2@2.34.24"
|
|
||||||
],
|
],
|
||||||
"shell": {
|
"shell": {
|
||||||
"init_hook": [
|
"init_hook": [
|
||||||
|
|||||||
@@ -772,7 +772,7 @@ Internet → Traefik → Service:8080 → Auth Sidecar:8080 → localhost → Yo
|
|||||||
Three authentication modes are supported:
|
Three authentication modes are supported:
|
||||||
1. **Token-based**: Static tokens (simple, good for service-to-service or internal apps)
|
1. **Token-based**: Static tokens (simple, good for service-to-service or internal apps)
|
||||||
2. **OIDC**: OpenID Connect (full SSO, good for user-facing apps)
|
2. **OIDC**: OpenID Connect (full SSO, good for user-facing apps)
|
||||||
3. **MCP**: OAuth 2.0 for MCP servers via RFC 9728 (Protected Resource Metadata); Keycloak provides native RFC 7591 Dynamic Client Registration (good for MCP tool servers requiring OAuth-based access control)
|
3. **MCP**: OAuth 2.0 for MCP servers via RFC 9728 / RFC 7591 (good for MCP tool servers requiring OAuth-based access control)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1013,7 +1013,7 @@ auth:
|
|||||||
scopes: "openid,profile,email" # OIDC scopes (optional)
|
scopes: "openid,profile,email" # OIDC scopes (optional)
|
||||||
callbackPath: /auth/callback # OAuth callback path (optional)
|
callbackPath: /auth/callback # OAuth callback path (optional)
|
||||||
|
|
||||||
# MCP mode configuration (RFC 9728)
|
# MCP mode configuration (RFC 9728 / RFC 7591)
|
||||||
mcp:
|
mcp:
|
||||||
resource: "" # Protected resource URL (required for MCP)
|
resource: "" # Protected resource URL (required for MCP)
|
||||||
authority: "" # Authorization server URL (required for MCP)
|
authority: "" # Authorization server URL (required for MCP)
|
||||||
@@ -1161,7 +1161,7 @@ ingress:
|
|||||||
host: mcp-server.forteapps.net
|
host: mcp-server.forteapps.net
|
||||||
```
|
```
|
||||||
|
|
||||||
The MCP auth mode implements RFC 9728 (OAuth 2.0 Protected Resource Metadata) for authorization server discovery. Dynamic Client Registration (RFC 7591) is handled natively by Keycloak; MCP clients discover the authorization server and scopes from the `/.well-known/oauth-protected-resource` endpoint served by the sidecar and then register directly with Keycloak.
|
The MCP auth mode implements RFC 9728 (OAuth 2.0 Protected Resource Metadata) for authorization server discovery and RFC 7591 (OAuth 2.0 Dynamic Client Registration) for automatic client registration. MCP clients discover the authorization server and scopes from the `/.well-known/oauth-protected-resource` endpoint served by the sidecar.
|
||||||
|
|
||||||
#### Example 4: Disabling Authentication
|
#### Example 4: Disabling Authentication
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Infrastructure Provisioning (OpenTofu)](#infrastructure-provisioning-opentofu)
|
|
||||||
- [Prerequisites](#provisioning-prerequisites)
|
|
||||||
- [Provisioning a Cluster](#provisioning-a-cluster)
|
|
||||||
- [Tearing Down a Cluster](#tearing-down-a-cluster)
|
|
||||||
- [Retrieving Kubeconfig](#retrieving-kubeconfig)
|
|
||||||
- [Platform Credentials](#platform-credentials)
|
|
||||||
- [Cluster Bootstrap](#cluster-bootstrap)
|
- [Cluster Bootstrap](#cluster-bootstrap)
|
||||||
- [Initial Cluster Setup](#initial-cluster-setup)
|
- [Initial Cluster Setup](#initial-cluster-setup)
|
||||||
- [ArgoCD Repository Access Setup](#argocd-repository-access-setup)
|
- [ArgoCD Repository Access Setup](#argocd-repository-access-setup)
|
||||||
@@ -35,120 +29,6 @@ This runbook provides operational procedures for maintaining the Kubernetes clus
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Infrastructure Provisioning (OpenTofu)
|
|
||||||
|
|
||||||
The `.tofu/` directory contains multi-cloud Kubernetes infrastructure-as-code using [OpenTofu](https://opentofu.org/). It provisions clusters on four cloud platforms (AKS, EKS, GKE, UpCloud), each with three environment tiers: **dev**, **prod**, and **workload**.
|
|
||||||
|
|
||||||
### Provisioning Prerequisites {#provisioning-prerequisites}
|
|
||||||
|
|
||||||
- **OpenTofu** (`tofu`) installed
|
|
||||||
- **kubectl** installed
|
|
||||||
- **helm** installed
|
|
||||||
- **yq** (optional — loads cluster config from `clusters/<cluster>.yaml`)
|
|
||||||
- Platform CLI tools:
|
|
||||||
- **AKS**: `az` (Azure CLI)
|
|
||||||
- **EKS**: `aws` (AWS CLI)
|
|
||||||
- **GKE**: `gcloud` (Google Cloud SDK)
|
|
||||||
- **UPC**: `upctl` (UpCloud CLI)
|
|
||||||
|
|
||||||
### Provisioning a Cluster
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Navigate to the scripts directory
|
|
||||||
cd .tofu/scripts
|
|
||||||
|
|
||||||
# 1. Copy and fill in credentials for your platform
|
|
||||||
cp ../configs/aks.env.example ../configs/aks.env
|
|
||||||
# Edit ../configs/aks.env with your credentials
|
|
||||||
|
|
||||||
# 2. Provision cluster (interactive — prompts before applying)
|
|
||||||
./setup-cluster.sh aks-dev
|
|
||||||
|
|
||||||
# 3. Dry-run only (plan without applying)
|
|
||||||
./setup-cluster.sh aks-dev --plan
|
|
||||||
|
|
||||||
# 4. Non-interactive (skip confirmations)
|
|
||||||
./setup-cluster.sh aks-dev --auto
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cluster name format**: `<platform>-<env>` — e.g., `aks-dev`, `eks-prod`, `gke-workload`, `upc-dev`
|
|
||||||
|
|
||||||
**What `setup-cluster.sh` does**:
|
|
||||||
1. Validates cluster name, extracts platform and environment
|
|
||||||
2. Checks prerequisites (tofu, kubectl, helm)
|
|
||||||
3. Loads credentials from `configs/<platform>.env`
|
|
||||||
4. Optionally loads cluster config from `clusters/<cluster>.yaml` (via yq)
|
|
||||||
5. Runs `tofu init` → `tofu plan` → prompts → `tofu apply`
|
|
||||||
6. Fetches and caches kubeconfig to `private/<cluster>/kubeconfig`
|
|
||||||
7. Waits for all nodes to reach Ready state (300s timeout)
|
|
||||||
8. Outputs next steps: `export KUBECONFIG` + `./bootstrap.sh`
|
|
||||||
|
|
||||||
### Tearing Down a Cluster
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Destroy cluster infrastructure
|
|
||||||
./teardown-cluster.sh aks-dev
|
|
||||||
|
|
||||||
# Equivalent to:
|
|
||||||
./setup-cluster.sh aks-dev --destroy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Retrieving Kubeconfig
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get kubeconfig for an existing cluster (uses cache or platform CLI)
|
|
||||||
./get-kubeconfig.sh aks-dev
|
|
||||||
|
|
||||||
# Cached kubeconfigs stored in: private/<cluster>/kubeconfig
|
|
||||||
```
|
|
||||||
|
|
||||||
Platform-specific retrieval fallbacks:
|
|
||||||
- **AKS**: `az aks get-credentials`
|
|
||||||
- **EKS**: `aws eks update-kubeconfig`
|
|
||||||
- **GKE**: `gcloud container clusters get-credentials`
|
|
||||||
- **UPC**: `upctl kubernetes config`
|
|
||||||
|
|
||||||
### Platform Credentials
|
|
||||||
|
|
||||||
Each platform has a `configs/<platform>.env.example` template. Copy to `.env` and populate:
|
|
||||||
|
|
||||||
| Platform | Required Variables | Optional |
|
|
||||||
|----------|--------------------|----------|
|
|
||||||
| **AKS** | `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID` | `ARM_RESOURCE_GROUP` (defaults to cluster name) |
|
|
||||||
| **EKS** | `AWS_PROFILE` (default: "default"), `AWS_REGION` (default: "eu-west-1") | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` |
|
|
||||||
| **GKE** | `GCP_PROJECT_ID`, `GCP_REGION` (default: "europe-west4") | `GOOGLE_APPLICATION_CREDENTIALS` (SA JSON path) |
|
|
||||||
| **UPC** | `UPCLOUD_TOKEN` | `UPCLOUD_CLUSTER_ID` (set after creation) |
|
|
||||||
|
|
||||||
> **Note**: `.env` files are git-ignored. Never commit credentials.
|
|
||||||
|
|
||||||
### End-to-End Workflow
|
|
||||||
|
|
||||||
Full cluster lifecycle: provision → bootstrap → operate → teardown:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Provision infrastructure
|
|
||||||
cd .tofu/scripts
|
|
||||||
./setup-cluster.sh aks-dev
|
|
||||||
|
|
||||||
# 2. Export kubeconfig (printed by setup-cluster.sh)
|
|
||||||
export KUBECONFIG=$(pwd)/../../private/aks-dev/kubeconfig
|
|
||||||
|
|
||||||
# 3. Bootstrap GitOps (ArgoCD + App-of-Apps)
|
|
||||||
cd ../..
|
|
||||||
./bootstrap.sh aks-dev
|
|
||||||
|
|
||||||
# 4. Verify
|
|
||||||
kubectl get applications -n argocd
|
|
||||||
|
|
||||||
# ... operate cluster ...
|
|
||||||
|
|
||||||
# 5. Teardown when done
|
|
||||||
cd .tofu/scripts
|
|
||||||
./teardown-cluster.sh aks-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cluster Bootstrap
|
## Cluster Bootstrap
|
||||||
|
|
||||||
### Initial Cluster Setup
|
### Initial Cluster Setup
|
||||||
@@ -157,7 +37,7 @@ Bootstrap a new cluster from scratch:
|
|||||||
|
|
||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
|
|
||||||
1. **Kubernetes cluster running** (provisioned via `.tofu/scripts/setup-cluster.sh` or manually on UpCloud, AWS EKS, Azure AKS, GCP GKE)
|
1. **Kubernetes cluster running** (UpCloud, AWS EKS, Azure AKS, GCP GKE, or any K8s cluster)
|
||||||
2. **kubectl configured** with admin access
|
2. **kubectl configured** with admin access
|
||||||
3. **Repositories cloned** locally
|
3. **Repositories cloned** locally
|
||||||
|
|
||||||
@@ -1406,17 +1286,14 @@ spec:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Provision new Kubernetes cluster
|
# 1. Provision new Kubernetes cluster
|
||||||
cd .tofu/scripts
|
|
||||||
./setup-cluster.sh upc-dev # or aks-dev, eks-prod, etc.
|
|
||||||
export KUBECONFIG=$(pwd)/../../private/upc-dev/kubeconfig
|
|
||||||
|
|
||||||
# 2. Verify cluster access
|
# 2. Configure kubectl
|
||||||
|
kubectl config use-context new-cluster
|
||||||
kubectl cluster-info
|
kubectl cluster-info
|
||||||
kubectl get nodes
|
|
||||||
|
|
||||||
# 3. Bootstrap cluster
|
# 3. Bootstrap cluster
|
||||||
cd ../..
|
cd ~/dev/k8s/launchpad
|
||||||
./bootstrap.sh upc-dev
|
./bootstrap.sh
|
||||||
|
|
||||||
# 4. Wait for ArgoCD to sync all applications
|
# 4. Wait for ArgoCD to sync all applications
|
||||||
kubectl get applications -n argocd -w
|
kubectl get applications -n argocd -w
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Architecture Components](#architecture-components)
|
- [Architecture Components](#architecture-components)
|
||||||
- [Repository Reference](#repository-reference)
|
- [Repository Reference](#repository-reference)
|
||||||
- [OpenTofu Infrastructure Reference](#opentofu-infrastructure-reference)
|
|
||||||
- [Helm Chart Reference](#helm-chart-reference)
|
- [Helm Chart Reference](#helm-chart-reference)
|
||||||
- [ArgoCD Configuration](#argocd-configuration)
|
- [ArgoCD Configuration](#argocd-configuration)
|
||||||
- [Infrastructure Components](#infrastructure-components)
|
- [Infrastructure Components](#infrastructure-components)
|
||||||
@@ -208,196 +207,6 @@ launchpad/
|
|||||||
└── REFERENCE.md
|
└── REFERENCE.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenTofu Infrastructure Reference
|
|
||||||
|
|
||||||
The `.tofu/` directory provides multi-cloud Kubernetes cluster provisioning using OpenTofu.
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
.tofu/
|
|
||||||
├── configs/ # Platform credential templates (git-ignored .env files)
|
|
||||||
│ ├── aks.env.example
|
|
||||||
│ ├── eks.env.example
|
|
||||||
│ ├── gke.env.example
|
|
||||||
│ └── upc.env.example
|
|
||||||
├── platforms/ # OpenTofu modules per cloud provider
|
|
||||||
│ ├── aks/ # Azure AKS
|
|
||||||
│ │ ├── modules/cluster/ # Reusable AKS module
|
|
||||||
│ │ │ ├── main.tf # Resource group, VNet, subnet, AKS cluster
|
|
||||||
│ │ │ ├── variables.tf
|
|
||||||
│ │ │ ├── outputs.tf
|
|
||||||
│ │ │ └── providers.tf
|
|
||||||
│ │ ├── dev/ # Dev environment root
|
|
||||||
│ │ ├── prod/ # Prod environment root
|
|
||||||
│ │ └── workload/ # Workload cluster (+ external-dns identity)
|
|
||||||
│ ├── eks/ # AWS EKS (same structure)
|
|
||||||
│ ├── gke/ # GCP GKE
|
|
||||||
│ └── upc/ # UpCloud Kubernetes
|
|
||||||
└── scripts/
|
|
||||||
├── setup-cluster.sh # Provision cluster
|
|
||||||
├── teardown-cluster.sh # Destroy cluster
|
|
||||||
└── get-kubeconfig.sh # Retrieve/cache kubeconfig
|
|
||||||
```
|
|
||||||
|
|
||||||
### Three-Tier Cluster Strategy
|
|
||||||
|
|
||||||
Each platform defines three environment tiers:
|
|
||||||
|
|
||||||
| Tier | Purpose | Typical Sizing | Notes |
|
|
||||||
|------|---------|---------------|-------|
|
|
||||||
| **dev** | Development/testing | Small, economical nodes (2 nodes) | No delete locks, minimal HA |
|
|
||||||
| **prod** | Production workloads | Larger nodes, multiple AZs (3 nodes) | Delete locks, HA networking |
|
|
||||||
| **workload** | Application-only cluster | Medium nodes (2 nodes) | Includes external-DNS integration, no platform services |
|
|
||||||
|
|
||||||
### Platform Specifications
|
|
||||||
|
|
||||||
#### AKS (Azure Kubernetes Service)
|
|
||||||
|
|
||||||
| Resource | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `azurerm_resource_group` | Container for all Azure resources |
|
|
||||||
| `azurerm_management_lock` | Optional CanNotDelete lock (prod) |
|
|
||||||
| `azurerm_virtual_network` | VPC, default `10.100.0.0/16` |
|
|
||||||
| `azurerm_subnet` | Node subnet, default `10.100.0.0/22` |
|
|
||||||
| `azurerm_kubernetes_cluster` | AKS with Azure CNI, OIDC issuer, Workload Identity |
|
|
||||||
|
|
||||||
**Dev**: Standard_B2s, 2 nodes, norwayeast, no delete lock
|
|
||||||
**Prod**: Standard_D4s_v3, 3 nodes, westeurope, delete lock enabled
|
|
||||||
**Workload**: Adds `azurerm_user_assigned_identity` + federated credential for external-dns with DNS Zone Contributor role
|
|
||||||
|
|
||||||
**Variables** (`modules/cluster/variables.tf`):
|
|
||||||
- `prefix` — resource name prefix
|
|
||||||
- `location` — Azure region
|
|
||||||
- `vnet_address_space` — default `10.100.0.0/16`
|
|
||||||
- `aks_subnet_cidr` — default `10.100.0.0/22`
|
|
||||||
- `aks_node_vm_size` — VM size (e.g., `Standard_B2s`)
|
|
||||||
- `aks_node_count` — number of nodes
|
|
||||||
- `aks_kubernetes_version` — `null` = latest
|
|
||||||
- `enable_delete_lock` — default `false`
|
|
||||||
|
|
||||||
#### EKS (Amazon Elastic Kubernetes Service)
|
|
||||||
|
|
||||||
| Resource | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `aws_vpc` | VPC with DNS enabled, default `10.100.0.0/16` |
|
|
||||||
| `aws_subnet` (public) | Per-AZ, tagged `kubernetes.io/role/elb=1` |
|
|
||||||
| `aws_subnet` (private) | Per-AZ, tagged `kubernetes.io/role/internal-elb=1` |
|
|
||||||
| `aws_nat_gateway` | Single NAT (dev); prod should use one per AZ |
|
|
||||||
| `aws_eks_cluster` | EKS with public+private endpoints, OIDC issuer |
|
|
||||||
| `aws_iam_openid_connect_provider` | IRSA (IAM Roles for Service Accounts) |
|
|
||||||
| `aws_eks_node_group` | Managed nodes with auto-scaling |
|
|
||||||
|
|
||||||
**Dev**: t3.medium, 2 nodes (min 1, max 4), eu-west-1a/b, K8s 1.30
|
|
||||||
**Prod**: m5.xlarge, 3 nodes (min 3, max 6), eu-west-1a/b/c
|
|
||||||
**Workload**: Adds IRSA role for external-dns with Route53 permissions (ChangeResourceRecordSets, ListHostedZones, ListResourceRecordSets, ListTagsForResource)
|
|
||||||
|
|
||||||
**Variables**:
|
|
||||||
- `region` — AWS region
|
|
||||||
- `vpc_cidr` — default `10.100.0.0/16`
|
|
||||||
- `availability_zones` — list of AZs (2–3 recommended)
|
|
||||||
- `node_instance_type`, `node_count`, `node_min_count`, `node_max_count`
|
|
||||||
- `kubernetes_version` — default `1.30`
|
|
||||||
|
|
||||||
#### GKE (Google Kubernetes Engine)
|
|
||||||
|
|
||||||
| Resource | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `google_project_service` | Enables compute and container APIs |
|
|
||||||
| `google_compute_network` | Custom VPC (no auto subnets) |
|
|
||||||
| `google_compute_subnetwork` | Primary `10.100.0.0/22`, pods `10.200.0.0/14`, services `10.204.0.0/20` |
|
|
||||||
| `google_container_cluster` | Regional cluster, VPC-native, Workload Identity |
|
|
||||||
| `google_container_node_pool` | Auto-repair, auto-upgrade, GKE_METADATA mode |
|
|
||||||
|
|
||||||
**Dev**: e2-standard-2, 2 nodes/zone, no deletion protection
|
|
||||||
**Prod**: e2-standard-4, 3 nodes/zone, deletion protection enabled
|
|
||||||
**Workload**: Adds Google SA for external-dns with `dns.admin` role + Workload Identity binding
|
|
||||||
|
|
||||||
**Variables**:
|
|
||||||
- `project_id` — GCP project (required)
|
|
||||||
- `region` — GCP region
|
|
||||||
- `node_machine_type`, `node_count`
|
|
||||||
- `kubernetes_version` — `null` = STABLE release channel
|
|
||||||
- `deletion_protection` — default `false`
|
|
||||||
|
|
||||||
#### UPC (UpCloud Kubernetes)
|
|
||||||
|
|
||||||
| Resource | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `upcloud_router` | Private router for cluster network |
|
|
||||||
| `upcloud_gateway` | NAT gateway for outbound internet |
|
|
||||||
| `upcloud_network` | Private network, DHCP, default `10.100.0.0/24` |
|
|
||||||
| `upcloud_kubernetes_cluster` | Managed K8s, private node groups |
|
|
||||||
| `upcloud_kubernetes_node_group` | Anti-affinity if node_count > 1 |
|
|
||||||
|
|
||||||
**Dev**: DEV-1xCPU-2GB, 2 nodes, no-svg1
|
|
||||||
**Prod**: 4xCPU-8GB, 3 nodes, no-svg1
|
|
||||||
**Workload**: 2xCPU-4GB, 2 nodes, no-svg1, CIDR `10.110.0.0/24`
|
|
||||||
|
|
||||||
> **Note**: UpCloud has no native workload identity — external-DNS integration not available.
|
|
||||||
|
|
||||||
### Workload Identity & External-DNS
|
|
||||||
|
|
||||||
Workload clusters include keyless cloud access for external-DNS:
|
|
||||||
|
|
||||||
| Platform | Identity Mechanism | DNS Permissions |
|
|
||||||
|----------|--------------------|-----------------|
|
|
||||||
| **AKS** | Azure Workload Identity (federated credential) | DNS Zone Contributor |
|
|
||||||
| **EKS** | IRSA (OIDC federation) | Route53 ChangeResourceRecordSets, ListHostedZones |
|
|
||||||
| **GKE** | Workload Identity (K8s SA → Google SA) | dns.admin role |
|
|
||||||
| **UPC** | N/A | N/A |
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
|
|
||||||
- Cluster: `<prefix>-aks` / `-eks` / `-gke` (derived from platform)
|
|
||||||
- Resource groups: `<prefix>-rg` (Azure only)
|
|
||||||
- VPCs/Networks: `<prefix>-vpc`
|
|
||||||
- Node groups: `<prefix>-nodes`
|
|
||||||
- Dev prefix: `clst-dev`, Prod prefix: `clst`, Workload prefix: `clst-workload`
|
|
||||||
|
|
||||||
### Provider Authentication
|
|
||||||
|
|
||||||
| Platform | Auth Method | Config Source |
|
|
||||||
|----------|-------------|---------------|
|
|
||||||
| **AKS** | Azure CLI or env vars (`ARM_SUBSCRIPTION_ID`, `ARM_TENANT_ID`) | `configs/aks.env` |
|
|
||||||
| **EKS** | AWS CLI profile or explicit credentials | `configs/eks.env` |
|
|
||||||
| **GKE** | Application Default Credentials or SA JSON | `configs/gke.env` |
|
|
||||||
| **UPC** | API token (`UPCLOUD_TOKEN`) | `configs/upc.env` |
|
|
||||||
|
|
||||||
### Scripts Reference
|
|
||||||
|
|
||||||
#### `setup-cluster.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./setup-cluster.sh <platform>-<env> [--plan] [--destroy] [--auto]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Flag | Effect |
|
|
||||||
|------|--------|
|
|
||||||
| (none) | Interactive: plan → prompt → apply |
|
|
||||||
| `--plan` | Dry-run only (tofu plan) |
|
|
||||||
| `--destroy` | Destroy infrastructure |
|
|
||||||
| `--auto` | Skip confirmation prompts |
|
|
||||||
|
|
||||||
#### `teardown-cluster.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./teardown-cluster.sh <platform>-<env>
|
|
||||||
# Delegates to: setup-cluster.sh "$@" --destroy
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `get-kubeconfig.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./get-kubeconfig.sh <platform>-<env>
|
|
||||||
# Checks cache: private/<cluster>/kubeconfig
|
|
||||||
# Falls back to platform CLI if no cache
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Key Files
|
#### Key Files
|
||||||
|
|
||||||
**`bootstrap.sh`**
|
**`bootstrap.sh`**
|
||||||
@@ -1736,7 +1545,7 @@ spec:
|
|||||||
2. `generate-auth-oidc-secret` - Creates Secret for OIDC mode
|
2. `generate-auth-oidc-secret` - Creates Secret for OIDC mode
|
||||||
3. `inject-sidecar-token` - Injects auth sidecar for token mode
|
3. `inject-sidecar-token` - Injects auth sidecar for token mode
|
||||||
4. `inject-sidecar-oidc` - Injects auth sidecar for OIDC mode
|
4. `inject-sidecar-oidc` - Injects auth sidecar for OIDC mode
|
||||||
5. `inject-sidecar-mcp` - Injects auth sidecar for MCP OAuth mode (RFC 9728)
|
5. `inject-sidecar-mcp` - Injects auth sidecar for MCP OAuth mode (RFC 9728 / RFC 7591)
|
||||||
6. `generate-auth-network-policy` - Creates NetworkPolicy to restrict ingress
|
6. `generate-auth-network-policy` - Creates NetworkPolicy to restrict ingress
|
||||||
|
|
||||||
#### Trigger Annotation
|
#### Trigger Annotation
|
||||||
@@ -1776,7 +1585,7 @@ policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar"
|
|||||||
policies.forteapps.io/auth-image-version: "latest"
|
policies.forteapps.io/auth-image-version: "latest"
|
||||||
```
|
```
|
||||||
|
|
||||||
**MCP Mode** (OAuth 2.0 for MCP servers, implements RFC 9728; MCP clients use Keycloak's native RFC 7591 endpoint for Dynamic Client Registration):
|
**MCP Mode** (OAuth 2.0 for MCP servers, implements RFC 9728 / RFC 7591):
|
||||||
```yaml
|
```yaml
|
||||||
# Annotations (required)
|
# Annotations (required)
|
||||||
policies.forteapps.io/auth: "true"
|
policies.forteapps.io/auth: "true"
|
||||||
@@ -2004,7 +1813,7 @@ Pod: Auth Sidecar (port 8080)
|
|||||||
├─ Validate credentials
|
├─ Validate credentials
|
||||||
│ • Token mode: Check Bearer token
|
│ • Token mode: Check Bearer token
|
||||||
│ • OIDC mode: Validate session or redirect to IdP
|
│ • OIDC mode: Validate session or redirect to IdP
|
||||||
│ • MCP mode: OAuth 2.0 via RFC 9728 discovery; Keycloak handles RFC 7591 dynamic registration natively
|
│ • MCP mode: OAuth 2.0 via RFC 9728 discovery / RFC 7591 dynamic registration
|
||||||
↓
|
↓
|
||||||
Forward to Application (localhost:3000)
|
Forward to Application (localhost:3000)
|
||||||
↓
|
↓
|
||||||
|
|||||||
@@ -1,30 +1,9 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- ../../base/cert-manager-application
|
- ../../base
|
||||||
- ../../base/cluster-resources-application
|
|
||||||
- ../../base/enterprise-apps
|
|
||||||
- ../../base/fluent-bit
|
|
||||||
- ../../base/gitea
|
|
||||||
- ../../base/gitea-actions
|
|
||||||
- ../../base/grafana
|
|
||||||
- ../../base/grafana-dashboards
|
|
||||||
- ../../base/homepage
|
|
||||||
- ../../base/karpor
|
|
||||||
- ../../base/keycloak
|
|
||||||
- ../../base/kyverno
|
|
||||||
- ../../base/kyverno-policies
|
|
||||||
- ../../base/loki
|
|
||||||
- ../../base/opencost
|
|
||||||
- ../../base/prometheus
|
|
||||||
- ../../base/renovate
|
|
||||||
- ../../base/sealedsecrets
|
|
||||||
- ../../base/tempo
|
|
||||||
- ../../base/traefik-application
|
|
||||||
- ../../base/vault
|
|
||||||
- vaultwarden-postgresql
|
- vaultwarden-postgresql
|
||||||
- vaultwarden
|
- vaultwarden
|
||||||
- wildcard-tls-certificate.yaml
|
|
||||||
|
|
||||||
# No patches needed — base already has "upc-dev" paths
|
# No patches needed — base already has "upc-dev" paths
|
||||||
# upc-dev is the default/base cluster
|
# upc-dev is the default/base cluster
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
# Wildcard Certificate for *.forteapps.net
|
|
||||||
# This creates a certificate that covers ALL subdomains of forteapps.net
|
|
||||||
# Once created, you can use it for any app like:
|
|
||||||
# - myapp.forteapps.net
|
|
||||||
# - api.forteapps.net
|
|
||||||
# - git.forteapps.net
|
|
||||||
# - vaultwarden.forteapps.net
|
|
||||||
# - etc.
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: wildcard-forteapps-net
|
|
||||||
namespace: cert-manager # Can be in any namespace, cert-manager namespace is common
|
|
||||||
spec:
|
|
||||||
# The secret where the TLS certificate will be stored
|
|
||||||
# This secret can be referenced by IngressRoutes in any namespace
|
|
||||||
secretName: wildcard-forteapps-net-tls
|
|
||||||
|
|
||||||
# Use the production issuer (use letsencrypt-staging for testing)
|
|
||||||
issuerRef:
|
|
||||||
name: letsencrypt-prod
|
|
||||||
kind: ClusterIssuer
|
|
||||||
|
|
||||||
# DNS names this certificate will cover
|
|
||||||
# Both wildcard AND apex domain are recommended
|
|
||||||
dnsNames:
|
|
||||||
- '*.forteapps.net' # Covers: myapp.forteapps.net, api.forteapps.net, etc.
|
|
||||||
- 'forteapps.net' # Also include apex domain explicitly
|
|
||||||
# Optional: Configure certificate duration and renewal
|
|
||||||
duration: 2160h0m0s # 90 days (Let's Encrypt default)
|
|
||||||
renewBefore: 720h0m0s # Renew 30 days before expiry
|
|
||||||
|
|
||||||
# Optional: Private key settings
|
|
||||||
privateKey:
|
|
||||||
algorithm: RSA
|
|
||||||
encoding: PKCS1
|
|
||||||
size: 4096
|
|
||||||
@@ -22,8 +22,7 @@ ingress:
|
|||||||
# TLS configuration
|
# TLS configuration
|
||||||
tls:
|
tls:
|
||||||
enabled: true # Set to true to enable TLS
|
enabled: true # Set to true to enable TLS
|
||||||
# secretName: "databunker-tls" # Name of the secret containing TLS certificate
|
secretName: "databunker-tls" # Name of the secret containing TLS certificate
|
||||||
secretName: "wildcard-forteapps-net-tls" # Name of the secret containing TLS certificate
|
|
||||||
|
|
||||||
# Pin PostgreSQL password — chart uses randAlphaNum without lookup,
|
# Pin PostgreSQL password — chart uses randAlphaNum without lookup,
|
||||||
# so each ArgoCD sync would regenerate the password while PVC keeps the old one.
|
# so each ArgoCD sync would regenerate the password while PVC keeps the old one.
|
||||||
|
|||||||
@@ -602,148 +602,3 @@ extraDeploy:
|
|||||||
items:
|
items:
|
||||||
- key: admin-password
|
- key: admin-password
|
||||||
path: admin-password
|
path: admin-password
|
||||||
|
|
||||||
# -- ServiceAccount for the client cleanup CronJob
|
|
||||||
- apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: keycloak-client-cleanup
|
|
||||||
namespace: keycloak
|
|
||||||
|
|
||||||
# -- CronJob: cleans up stale dynamically registered Keycloak clients
|
|
||||||
- apiVersion: batch/v1
|
|
||||||
kind: CronJob
|
|
||||||
metadata:
|
|
||||||
name: keycloak-client-cleanup
|
|
||||||
namespace: keycloak
|
|
||||||
spec:
|
|
||||||
schedule: "0 3 * * 0"
|
|
||||||
concurrencyPolicy: Forbid
|
|
||||||
successfulJobsHistoryLimit: 1
|
|
||||||
failedJobsHistoryLimit: 3
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
backoffLimit: 3
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
serviceAccountName: keycloak-client-cleanup
|
|
||||||
restartPolicy: Never
|
|
||||||
containers:
|
|
||||||
- name: cleanup
|
|
||||||
image: alpine:3.20
|
|
||||||
command: [ "/bin/sh", "-c" ]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
apk add --no-cache curl jq > /dev/null 2>&1
|
|
||||||
|
|
||||||
KEYCLOAK_URL="http://keycloak:80"
|
|
||||||
REALM="forte"
|
|
||||||
ADMIN_USER="admin"
|
|
||||||
ADMIN_PASS=$(cat /secrets/admin-password)
|
|
||||||
DRY_RUN="${DRY_RUN:-true}"
|
|
||||||
MIN_AGE_DAYS="${MIN_AGE_DAYS:-7}"
|
|
||||||
if [ -z "$CLIENT_ID_PATTERN" ]; then
|
|
||||||
CLIENT_ID_PATTERN='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== Keycloak DCR client cleanup ==="
|
|
||||||
echo "Dry run: ${DRY_RUN}"
|
|
||||||
echo "Min age: ${MIN_AGE_DAYS} days"
|
|
||||||
echo "Pattern: ${CLIENT_ID_PATTERN}"
|
|
||||||
|
|
||||||
# Authenticate to Keycloak Admin API
|
|
||||||
TOKEN=$(curl -sf -X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
|
|
||||||
-d "client_id=admin-cli" \
|
|
||||||
-d "username=${ADMIN_USER}" \
|
|
||||||
-d "password=${ADMIN_PASS}" \
|
|
||||||
-d "grant_type=password" | jq -r '.access_token')
|
|
||||||
|
|
||||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
|
||||||
echo "ERROR: Failed to authenticate to Keycloak"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
NOW_SEC=$(date +%s)
|
|
||||||
MIN_AGE_SEC=$((MIN_AGE_DAYS * 86400))
|
|
||||||
|
|
||||||
# Hardcoded protected clients (never delete these)
|
|
||||||
PROTECTED_JSON='["gitea","grafana","argocd","vaultwarden","account","account-console","admin-cli","broker","realm-management","security-admin-console"]'
|
|
||||||
|
|
||||||
echo "Fetching clients from realm '${REALM}'..."
|
|
||||||
CLIENTS=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \
|
|
||||||
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients")
|
|
||||||
|
|
||||||
CANDIDATES=$(echo "$CLIENTS" | jq -c --argjson protected "$PROTECTED_JSON" --argjson now "$NOW_SEC" --argjson min_age "$MIN_AGE_SEC" --arg pattern "$CLIENT_ID_PATTERN" '
|
|
||||||
[
|
|
||||||
.[]
|
|
||||||
| select(.clientId as $cid | $protected | index($cid) | not)
|
|
||||||
| select(.attributes["k8s.secret.sync"] != "true")
|
|
||||||
| select(.clientId | test($pattern; "i"))
|
|
||||||
| select((.createdTimestamp // 0) / 1000 < ($now - $min_age))
|
|
||||||
]
|
|
||||||
')
|
|
||||||
|
|
||||||
COUNT=$(echo "$CANDIDATES" | jq 'length')
|
|
||||||
echo "Found ${COUNT} candidate client(s) matching pattern and age threshold"
|
|
||||||
|
|
||||||
if [ "$COUNT" -eq 0 ]; then
|
|
||||||
echo "Nothing to clean up."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r CLIENT; do
|
|
||||||
CLIENT_ID=$(echo "$CLIENT" | jq -r '.clientId')
|
|
||||||
CLIENT_UUID=$(echo "$CLIENT" | jq -r '.id')
|
|
||||||
CREATED=$(echo "$CLIENT" | jq -r '.createdTimestamp')
|
|
||||||
|
|
||||||
# Check active sessions
|
|
||||||
SESSION_RESPONSE=$(curl -s -H "Authorization: Bearer ${TOKEN}" \
|
|
||||||
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/session-count" || true)
|
|
||||||
SESSION_COUNT=$(echo "$SESSION_RESPONSE" | jq -r '.count // 0')
|
|
||||||
|
|
||||||
if [ "$SESSION_COUNT" -gt 0 ]; then
|
|
||||||
echo " SKIP: '${CLIENT_ID}' has ${SESSION_COUNT} active session(s)"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
AGE_DAYS=$(( (NOW_SEC - (CREATED / 1000)) / 86400 ))
|
|
||||||
|
|
||||||
if [ "$DRY_RUN" = "true" ]; then
|
|
||||||
echo " DRY-RUN: would delete '${CLIENT_ID}' (uuid: ${CLIENT_UUID}, age: ${AGE_DAYS}d)"
|
|
||||||
else
|
|
||||||
echo " DELETE: '${CLIENT_ID}' (uuid: ${CLIENT_UUID}, age: ${AGE_DAYS}d)"
|
|
||||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}" \
|
|
||||||
-X DELETE \
|
|
||||||
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}" || echo "000")
|
|
||||||
if [ "$HTTP_CODE" != "204" ] && [ "$HTTP_CODE" != "200" ]; then
|
|
||||||
echo " ERROR: Failed to delete '${CLIENT_ID}' (HTTP ${HTTP_CODE})"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done < <(echo "$CANDIDATES" | jq -c '.[]')
|
|
||||||
|
|
||||||
echo "Cleanup run complete."
|
|
||||||
env:
|
|
||||||
- name: DRY_RUN
|
|
||||||
value: "false"
|
|
||||||
- name: MIN_AGE_DAYS
|
|
||||||
value: "7"
|
|
||||||
volumeMounts:
|
|
||||||
- name: keycloak-credentials
|
|
||||||
mountPath: /secrets
|
|
||||||
readOnly: true
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 64Mi
|
|
||||||
limits:
|
|
||||||
cpu: 200m
|
|
||||||
memory: 128Mi
|
|
||||||
volumes:
|
|
||||||
- name: keycloak-credentials
|
|
||||||
secret:
|
|
||||||
secretName: keycloak-credentials
|
|
||||||
items:
|
|
||||||
- key: admin-password
|
|
||||||
path: admin-password
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: bunker.forteapps.net
|
host: databunker.forteapps.net
|
||||||
annotations:
|
annotations:
|
||||||
gethomepage.dev/enabled: "true"
|
gethomepage.dev/enabled: "true"
|
||||||
gethomepage.dev/name: "Databunker"
|
gethomepage.dev/name: "Databunker"
|
||||||
gethomepage.dev/description: "Secure Database for PII and PCI Records"
|
gethomepage.dev/description: "Secure Database for PII and PCI Records"
|
||||||
gethomepage.dev/group: "Security"
|
gethomepage.dev/group: "Security"
|
||||||
gethomepage.dev/icon: "double-take"
|
gethomepage.dev/icon: "double-take"
|
||||||
gethomepage.dev/href: "https://bunker.forteapps.net"
|
gethomepage.dev/href: "https://databunker.forteapps.net"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ config:
|
|||||||
description: Teknisk kompetanse fra offentlige anbud
|
description: Teknisk kompetanse fra offentlige anbud
|
||||||
icon: forte
|
icon: forte
|
||||||
- Forte Drop:
|
- Forte Drop:
|
||||||
href: https://drop.forteapps.net
|
href: https://drop.hackathon.forteapps.net
|
||||||
description: Self-hosted HTML-drops + MCP for Claude
|
description: Self-hosted HTML-drops + MCP for Claude
|
||||||
icon: forte
|
icon: forte
|
||||||
- Forte Feedback:
|
- Forte Feedback:
|
||||||
|
|||||||
Reference in New Issue
Block a user