migrate Vault and Gitea to GKE Autopilot cluster
Implements Option A: move Vault (3x GCE e2-small) and Gitea (Legion k8s) onto a new GKE Autopilot cluster (neuron-platform, us-central1) managed through Legion Argo CD. Terraform (servers/gcp/): - gke.tf: GKE Autopilot cluster, Workload Identity bindings for Vault (KMS) and Gitea (Cloud SQL) - cloud-sql.tf: gitea database + user on neuron-prod-pg15, gitea GCP SA, gitea-database-url and gitea-db-password Secret Manager secrets - vault-nodes.tf: PENDING DECOMMISSION comment with migration checklist k8s manifests (servers/gcp/k8s/): - vault/: namespace.yaml - gitea/: namespace, serviceaccount (Workload Identity annotation), pvc (50Gi standard-rwo), deployment (Gitea + Cloud SQL Auth Proxy sidecar), service, configmap (custom CSS carried from Legion), external-secrets (GCP SM provider) - argocd-apps/: vault-gke.yaml, vault-helm-gke.yaml (Helm chart, HA Raft 3 replicas, GCP KMS auto-unseal, topologySpread across zones, 10Gi premium-rwo), gitea-gke.yaml — all target GKE_CLUSTER_ENDPOINT placeholder Legion (servers/legion/): - apps/gke-apps.yaml: App-of-Apps entry point on Legion Argo CD that syncs the GKE Application manifests - k8s/gitea-runner/Dockerfile: add system-level git insteadOf so GKE CI runners resolve Gitea in-cluster without Cloudflare Access headers
This commit is contained in:
@@ -148,3 +148,100 @@ resource "google_secret_manager_secret" "license_admin_token" {
|
||||
ignore_changes = [id]
|
||||
}
|
||||
}
|
||||
|
||||
# ── Gitea — database + user on neuron-prod-pg15 ───────────────────────────────
|
||||
# Gitea on GKE uses the existing Cloud SQL instance with a Cloud SQL Auth Proxy
|
||||
# sidecar. Connection via unix socket — no public IP exposure.
|
||||
|
||||
resource "google_sql_database" "gitea" {
|
||||
name = "gitea"
|
||||
instance = google_sql_database_instance.main.name
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
resource "random_password" "gitea_db" {
|
||||
length = 32
|
||||
special = false # Cloud SQL unix socket path DSN; keep alphanumeric for simplicity
|
||||
}
|
||||
|
||||
resource "google_sql_user" "gitea" {
|
||||
name = "gitea"
|
||||
instance = google_sql_database_instance.main.name
|
||||
project = var.project_id
|
||||
password = random_password.gitea_db.result
|
||||
}
|
||||
|
||||
# ── Gitea service account (for Workload Identity → Cloud SQL) ─────────────────
|
||||
|
||||
resource "google_service_account" "gitea" {
|
||||
account_id = "gitea"
|
||||
display_name = "Gitea GKE SA"
|
||||
description = "Service account for the Gitea pod on GKE. Used by the Cloud SQL Auth Proxy sidecar via Workload Identity."
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
resource "google_project_iam_member" "gitea_sql_client" {
|
||||
project = var.project_id
|
||||
role = "roles/cloudsql.client"
|
||||
member = "serviceAccount:${google_service_account.gitea.email}"
|
||||
}
|
||||
|
||||
# ── Secret Manager — Gitea database URL ───────────────────────────────────────
|
||||
# Full DSN using the Cloud SQL Auth Proxy unix socket path.
|
||||
# The proxy sidecar mounts the socket at /cloudsql/<connection_name>.
|
||||
|
||||
resource "google_secret_manager_secret" "gitea_database_url" {
|
||||
secret_id = "gitea-database-url"
|
||||
project = var.project_id
|
||||
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "gitea_database_url" {
|
||||
secret = google_secret_manager_secret.gitea_database_url.id
|
||||
secret_data = "host=/cloudsql/${google_sql_database_instance.main.connection_name} user=gitea password=${random_password.gitea_db.result} dbname=gitea sslmode=disable"
|
||||
}
|
||||
|
||||
# gitea-db-password — the raw password only, for use in Gitea's GITEA__database__PASSWD.
|
||||
# The Cloud SQL Auth Proxy provides the unix socket; Gitea uses standard postgres
|
||||
# password auth through the proxy socket.
|
||||
resource "google_secret_manager_secret" "gitea_db_password" {
|
||||
secret_id = "gitea-db-password"
|
||||
project = var.project_id
|
||||
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "gitea_db_password" {
|
||||
secret = google_secret_manager_secret.gitea_db_password.id
|
||||
secret_data = random_password.gitea_db.result
|
||||
}
|
||||
|
||||
# Allow the Gitea GCP SA to access its secrets
|
||||
resource "google_secret_manager_secret_iam_member" "gitea_database_url_accessor" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.gitea_database_url.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gitea.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "gitea_db_password_accessor" {
|
||||
project = var.project_id
|
||||
secret_id = google_secret_manager_secret.gitea_db_password.secret_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.gitea.email}"
|
||||
}
|
||||
|
||||
output "gitea_service_account_email" {
|
||||
description = "Gitea GKE SA email — used in the Workload Identity annotation"
|
||||
value = google_service_account.gitea.email
|
||||
}
|
||||
|
||||
output "gitea_database_secret_id" {
|
||||
description = "Secret Manager secret ID for the Gitea database URL"
|
||||
value = google_secret_manager_secret.gitea_database_url.secret_id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# ── GKE Autopilot Cluster — neuron-platform ───────────────────────────────────
|
||||
#
|
||||
# Regional Autopilot cluster in us-central1. Autopilot manages the node pool
|
||||
# automatically — no node pools to configure, no VM sizes to choose.
|
||||
# Pods spread across a/b/c zones automatically by GKE.
|
||||
#
|
||||
# Workload Identity is required so Vault pods can access KMS (auto-unseal)
|
||||
# and Gitea pods can use Cloud SQL Auth Proxy without key files.
|
||||
#
|
||||
# After `terraform apply`, register the cluster with Legion Argo CD:
|
||||
# 1. Get the endpoint: terraform output gke_cluster_endpoint
|
||||
# 2. Get credentials: gcloud container clusters get-credentials neuron-platform \
|
||||
# --region us-central1 --project neuron-785695
|
||||
# 3. Register with Argo CD:
|
||||
# argocd cluster add <context-name> --name gke-neuron-platform
|
||||
# 4. Update servers/gcp/k8s/argocd-apps/*.yaml with the actual cluster endpoint.
|
||||
|
||||
resource "google_container_cluster" "neuron_platform" {
|
||||
provider = google-beta
|
||||
|
||||
name = "neuron-platform"
|
||||
location = "us-central1"
|
||||
project = var.project_id
|
||||
|
||||
# Autopilot — GKE manages node pools, scaling, and node security
|
||||
enable_autopilot = true
|
||||
|
||||
release_channel {
|
||||
channel = "REGULAR"
|
||||
}
|
||||
|
||||
# Workload Identity — required for Vault KMS and Gitea Cloud SQL proxy
|
||||
workload_identity_config {
|
||||
workload_pool = "${var.project_id}.svc.id.goog"
|
||||
}
|
||||
|
||||
# Deletion protection — prevent accidental cluster destruction
|
||||
deletion_protection = true
|
||||
|
||||
# Cluster networking — uses default VPC (same as Vault GCE nodes)
|
||||
ip_allocation_policy {}
|
||||
|
||||
# Disable basic auth and client cert (Workload Identity handles auth)
|
||||
master_auth {
|
||||
client_certificate_config {
|
||||
issue_client_certificate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Workload Identity bindings ────────────────────────────────────────────────
|
||||
#
|
||||
# Each GCP SA gets a binding that allows its corresponding k8s ServiceAccount
|
||||
# (in the GKE cluster) to impersonate it via Workload Identity.
|
||||
# The k8s SA must also have the annotation:
|
||||
# iam.gke.io/gcp-service-account: <gcp-sa-email>
|
||||
|
||||
# Vault pods (namespace: vault, k8s SA: vault) → vault-unseal GCP SA
|
||||
# This lets the Vault StatefulSet access KMS for auto-unseal without key files.
|
||||
resource "google_service_account_iam_member" "vault_workload_identity" {
|
||||
service_account_id = google_service_account.vault_unseal.name
|
||||
role = "roles/iam.workloadIdentityUser"
|
||||
member = "serviceAccount:${var.project_id}.svc.id.goog[vault/vault]"
|
||||
|
||||
depends_on = [google_container_cluster.neuron_platform]
|
||||
}
|
||||
|
||||
# Gitea pod (namespace: gitea, k8s SA: gitea) → gitea GCP SA
|
||||
# This lets the Cloud SQL Auth Proxy sidecar connect to neuron-prod-pg15.
|
||||
resource "google_service_account_iam_member" "gitea_workload_identity" {
|
||||
service_account_id = google_service_account.gitea.name
|
||||
role = "roles/iam.workloadIdentityUser"
|
||||
member = "serviceAccount:${var.project_id}.svc.id.goog[gitea/gitea]"
|
||||
|
||||
depends_on = [google_container_cluster.neuron_platform]
|
||||
}
|
||||
|
||||
# ── Outputs ───────────────────────────────────────────────────────────────────
|
||||
|
||||
output "gke_cluster_endpoint" {
|
||||
description = "GKE cluster API endpoint — use as destination.server in Argo CD Applications"
|
||||
value = "https://${google_container_cluster.neuron_platform.endpoint}"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "gke_cluster_name" {
|
||||
description = "GKE cluster name"
|
||||
value = google_container_cluster.neuron_platform.name
|
||||
}
|
||||
|
||||
output "gke_workload_pool" {
|
||||
description = "Workload Identity pool — used in Workload Identity bindings"
|
||||
value = "${var.project_id}.svc.id.goog"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: gitea-gke
|
||||
namespace: argocd
|
||||
annotations:
|
||||
# Apply to the Legion Argo CD instance after registering the GKE cluster.
|
||||
# See vault-gke.yaml for the cluster registration steps.
|
||||
#
|
||||
# Migration checklist (Gitea from Legion to GKE):
|
||||
# 1. terraform apply (creates GKE cluster, Cloud SQL gitea DB, secrets)
|
||||
# 2. Register GKE cluster with Legion Argo CD (see vault-gke.yaml)
|
||||
# 3. Install ESO on GKE: helm install external-secrets external-secrets/external-secrets
|
||||
# --namespace external-secrets --create-namespace
|
||||
# 4. Apply this Application to Legion Argo CD
|
||||
# 5. Verify gitea pod is running on GKE with DB connectivity
|
||||
# 6. Take a tar of /data from the Legion Gitea pod and restore to GKE PVC
|
||||
# 7. Update Cloudflare Tunnel on Legion: remove git.neuralplatform.ai route
|
||||
# 8. Add GKE ingress / GCP LB rule for git.neuralplatform.ai
|
||||
# 9. Decommission Gitea on Legion (remove gitea*.yaml from servers/legion/apps/)
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git
|
||||
targetRevision: main
|
||||
path: servers/gcp/k8s/gitea
|
||||
destination:
|
||||
# Replace GKE_CLUSTER_ENDPOINT after `terraform apply`:
|
||||
# terraform -chdir=servers/gcp output -raw gke_cluster_endpoint
|
||||
server: https://GKE_CLUSTER_ENDPOINT
|
||||
namespace: gitea
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- ServerSideApply=true
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: vault-gke
|
||||
namespace: argocd
|
||||
annotations:
|
||||
# Syncs plain k8s manifests for Vault to the GKE cluster.
|
||||
# The Vault Helm release itself is in vault-helm-gke.yaml.
|
||||
#
|
||||
# Apply to Legion Argo CD after registering the GKE cluster:
|
||||
# 1. gcloud container clusters get-credentials neuron-platform \
|
||||
# --region us-central1 --project neuron-785695
|
||||
# 2. argocd cluster add <context-name> --name gke-neuron-platform
|
||||
# 3. Update GKE_CLUSTER_ENDPOINT in all argocd-apps/*.yaml:
|
||||
# terraform -chdir=servers/gcp output -raw gke_cluster_endpoint
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git
|
||||
targetRevision: main
|
||||
path: servers/gcp/k8s/vault
|
||||
destination:
|
||||
# Replace GKE_CLUSTER_ENDPOINT after `terraform apply`:
|
||||
# terraform -chdir=servers/gcp output -raw gke_cluster_endpoint
|
||||
server: https://GKE_CLUSTER_ENDPOINT
|
||||
namespace: vault
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- ServerSideApply=true
|
||||
@@ -0,0 +1,153 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: vault-helm-gke
|
||||
namespace: argocd
|
||||
annotations:
|
||||
# Deploys the Vault Helm chart to the GKE cluster via Legion Argo CD.
|
||||
# The destination.server must be updated after `terraform apply`:
|
||||
# terraform -chdir=servers/gcp output -raw gke_cluster_endpoint
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: https://helm.releases.hashicorp.com
|
||||
chart: vault
|
||||
targetRevision: "0.29.1"
|
||||
helm:
|
||||
values: |
|
||||
global:
|
||||
enabled: true
|
||||
|
||||
injector:
|
||||
enabled: false
|
||||
|
||||
ui:
|
||||
enabled: true
|
||||
|
||||
server:
|
||||
image:
|
||||
repository: hashicorp/vault
|
||||
tag: "1.19.2"
|
||||
|
||||
# Workload Identity — Vault pod k8s SA impersonates vault-unseal GCP SA
|
||||
# for KMS auto-unseal. Binding is in servers/gcp/gke.tf.
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: vault
|
||||
annotations:
|
||||
iam.gke.io/gcp-service-account: vault-unseal@neuron-785695.iam.gserviceaccount.com
|
||||
|
||||
# GKE Autopilot: no privileged containers. Vault doesn't need privilege.
|
||||
# Request IPC_LOCK so Vault can lock memory (prevents secrets swap).
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- IPC_LOCK
|
||||
|
||||
# HA mode — 3 replicas with Raft storage
|
||||
ha:
|
||||
enabled: true
|
||||
replicas: 3
|
||||
raft:
|
||||
enabled: true
|
||||
setNodeId: true
|
||||
config: |
|
||||
ui = true
|
||||
|
||||
listener "tcp" {
|
||||
tls_disable = 1
|
||||
address = "[::]:8200"
|
||||
cluster_address = "[::]:8201"
|
||||
}
|
||||
|
||||
storage "raft" {
|
||||
path = "/vault/data"
|
||||
|
||||
retry_join {
|
||||
leader_api_addr = "http://vault-0.vault-internal:8200"
|
||||
}
|
||||
retry_join {
|
||||
leader_api_addr = "http://vault-1.vault-internal:8200"
|
||||
}
|
||||
retry_join {
|
||||
leader_api_addr = "http://vault-2.vault-internal:8200"
|
||||
}
|
||||
}
|
||||
|
||||
seal "gcpckms" {
|
||||
project = "neuron-785695"
|
||||
region = "global"
|
||||
key_ring = "vault"
|
||||
crypto_key = "vault-unseal"
|
||||
}
|
||||
|
||||
telemetry {
|
||||
prometheus_retention_time = "30s"
|
||||
disable_hostname = false
|
||||
}
|
||||
|
||||
# Spread pods across GKE zones
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
topologyKey: topology.kubernetes.io/zone
|
||||
whenUnsatisfiable: DoNotSchedule
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: vault
|
||||
component: server
|
||||
|
||||
# 10Gi SSD per pod — premium-rwo = pd-ssd on GKE Autopilot
|
||||
dataStorage:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
storageClass: premium-rwo
|
||||
accessMode: ReadWriteOnce
|
||||
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
path: "/v1/sys/health?standbyok=true&sealedok=true&uninitcode=200"
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
path: "/v1/sys/health?standbyok=true&sealedok=true&uninitcode=200"
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
|
||||
# GKE Autopilot requires resource requests on all containers
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 250m
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
port: 8200
|
||||
targetPort: 8200
|
||||
|
||||
# Ingress disabled — Vault is exposed via GCP HTTPS LB.
|
||||
# After migration, update the existing LB backend (vault-nodes.tf)
|
||||
# to target a GKE NEG instead of the GCE instance groups.
|
||||
# See: https://cloud.google.com/kubernetes-engine/docs/how-to/standalone-neg
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
destination:
|
||||
# Replace GKE_CLUSTER_ENDPOINT after `terraform apply`:
|
||||
# terraform -chdir=servers/gcp output -raw gke_cluster_endpoint
|
||||
server: https://GKE_CLUSTER_ENDPOINT
|
||||
namespace: vault
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- ServerSideApply=true
|
||||
@@ -0,0 +1,104 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: gitea-custom-css
|
||||
namespace: gitea
|
||||
data:
|
||||
header.tmpl: |
|
||||
<link rel="stylesheet" href="/assets/css/custom.css">
|
||||
custom.css: |
|
||||
/* ── Typography & base ── */
|
||||
:root {
|
||||
--color-primary: #6366f1;
|
||||
--color-primary-dark: #4f46e5;
|
||||
--color-secondary: #8b5cf6;
|
||||
--color-bg: #0f0f17;
|
||||
--color-surface: #16161f;
|
||||
--color-surface-2: #1e1e2e;
|
||||
--color-border: #2a2a3d;
|
||||
--color-text: #e2e2f0;
|
||||
--color-text-muted: #8888aa;
|
||||
--color-green: #22d3a5;
|
||||
--radius: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg) !important;
|
||||
color: var(--color-text) !important;
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
|
||||
}
|
||||
|
||||
/* ── Top navbar ── */
|
||||
.navbar, #navbar {
|
||||
background: var(--color-surface) !important;
|
||||
border-bottom: 1px solid var(--color-border) !important;
|
||||
box-shadow: 0 1px 12px rgba(0,0,0,0.4) !important;
|
||||
}
|
||||
|
||||
.navbar .brand svg, .navbar .brand img { filter: brightness(1.2); }
|
||||
|
||||
/* ── Sidebar & panels ── */
|
||||
.repository, .ui.container, .ui.segment,
|
||||
.ui.card, .ui.cards > .card {
|
||||
background: var(--color-surface) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.ui.primary.button, .ui.green.button {
|
||||
background: var(--color-primary) !important;
|
||||
border: none !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
.ui.primary.button:hover { background: var(--color-primary-dark) !important; }
|
||||
|
||||
/* ── Inputs ── */
|
||||
input, textarea, select,
|
||||
.ui.input > input, .ui.dropdown {
|
||||
background: var(--color-surface-2) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
color: var(--color-text) !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
/* ── Sign-in page ── */
|
||||
.user.signin .ui.segment,
|
||||
.user.signup .ui.segment {
|
||||
background: var(--color-surface) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 16px !important;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5) !important;
|
||||
padding: 2.5rem !important;
|
||||
}
|
||||
|
||||
/* ── Repo file tree ── */
|
||||
.repository.file.list .file-list {
|
||||
background: var(--color-surface) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
}
|
||||
|
||||
.repository.file.list .file-list tr:hover td {
|
||||
background: var(--color-surface-2) !important;
|
||||
}
|
||||
|
||||
/* ── Labels & badges ── */
|
||||
.ui.label { border-radius: 6px !important; }
|
||||
|
||||
/* ── Dashboard activity feed ── */
|
||||
.feeds .news { border-bottom: 1px solid var(--color-border) !important; }
|
||||
|
||||
/* ── Code blocks ── */
|
||||
pre, code {
|
||||
background: var(--color-surface-2) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
/* ── Muted footer ── */
|
||||
#footer {
|
||||
background: var(--color-surface) !important;
|
||||
border-top: 1px solid var(--color-border) !important;
|
||||
color: var(--color-text-muted) !important;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
labels:
|
||||
app: gitea
|
||||
spec:
|
||||
replicas: 1
|
||||
# Recreate — only one pod can hold the RWO PVC at a time.
|
||||
# Scale to multiple replicas only after adding shared storage (e.g. Filestore).
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitea
|
||||
spec:
|
||||
serviceAccountName: gitea
|
||||
|
||||
containers:
|
||||
- name: gitea
|
||||
image: gitea/gitea:1.25.5
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
- name: ssh
|
||||
containerPort: 22
|
||||
env:
|
||||
# Database — connect through the Cloud SQL Auth Proxy unix socket
|
||||
- name: GITEA__database__DB_TYPE
|
||||
value: postgres
|
||||
# Unix socket path used by the Cloud SQL Auth Proxy sidecar
|
||||
- name: GITEA__database__HOST
|
||||
value: /cloudsql/neuron-785695:us-central1:neuron-prod-pg15
|
||||
- name: GITEA__database__NAME
|
||||
value: gitea
|
||||
- name: GITEA__database__USER
|
||||
value: gitea
|
||||
- name: GITEA__database__PASSWD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gitea-db
|
||||
key: password
|
||||
# Server
|
||||
- name: GITEA__server__DOMAIN
|
||||
value: git.neuralplatform.ai
|
||||
- name: GITEA__server__ROOT_URL
|
||||
value: https://git.neuralplatform.ai
|
||||
- name: GITEA__server__SSH_DOMAIN
|
||||
value: git.neuralplatform.ai
|
||||
- name: GITEA__server__SSH_PORT
|
||||
value: "22"
|
||||
- name: GITEA__server__START_SSH_SERVER
|
||||
value: "false"
|
||||
# Service
|
||||
- name: GITEA__service__DISABLE_REGISTRATION
|
||||
value: "true"
|
||||
- name: GITEA__service__REQUIRE_SIGNIN_VIEW
|
||||
value: "false"
|
||||
# Security
|
||||
- name: GITEA__security__INSTALL_LOCK
|
||||
value: "true"
|
||||
# Packages
|
||||
- name: GITEA__packages__ENABLED
|
||||
value: "true"
|
||||
# Webhooks — allow calls back into the cluster and GKE VPC
|
||||
- name: GITEA__webhook__ALLOWED_HOST_LIST
|
||||
value: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
# Actions
|
||||
- name: GITEA__actions__DEFAULT_ACTIONS_URL
|
||||
value: https://code.forgejo.org
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: cloudsql-socket
|
||||
mountPath: /cloudsql
|
||||
- name: custom-css
|
||||
mountPath: /data/gitea/custom/public/assets/css/custom.css
|
||||
subPath: custom.css
|
||||
- name: custom-css
|
||||
mountPath: /data/gitea/custom/templates/custom/header.tmpl
|
||||
subPath: header.tmpl
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
failureThreshold: 3
|
||||
|
||||
# Cloud SQL Auth Proxy sidecar — provides unix socket at /cloudsql/
|
||||
# Authenticates to Cloud SQL using Workload Identity (no key file).
|
||||
- name: cloud-sql-proxy
|
||||
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2
|
||||
args:
|
||||
- "--structured-logs"
|
||||
- "--unix-socket=/cloudsql"
|
||||
- "neuron-785695:us-central1:neuron-prod-pg15"
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
volumeMounts:
|
||||
- name: cloudsql-socket
|
||||
mountPath: /cloudsql
|
||||
resources:
|
||||
requests:
|
||||
memory: 32Mi
|
||||
cpu: 10m
|
||||
limits:
|
||||
memory: 128Mi
|
||||
cpu: 100m
|
||||
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: gitea-data
|
||||
- name: cloudsql-socket
|
||||
emptyDir: {}
|
||||
- name: custom-css
|
||||
configMap:
|
||||
name: gitea-custom-css
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
spec:
|
||||
selector:
|
||||
app: gitea
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
- name: ssh
|
||||
port: 22
|
||||
targetPort: 22
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
# SecretStore for GKE — uses GCP Secret Manager directly via Workload Identity.
|
||||
# On GKE we use the GCP provider instead of a Vault-backed store, since
|
||||
# Vault itself may be in the process of being migrated.
|
||||
# The Gitea GCP SA has secretmanager.secretAccessor on its own secret (see cloud-sql.tf).
|
||||
#
|
||||
# Pre-requisite: install ESO on GKE before applying this:
|
||||
# helm install external-secrets external-secrets/external-secrets \
|
||||
# --namespace external-secrets --create-namespace
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: SecretStore
|
||||
metadata:
|
||||
name: gcp-secretmanager
|
||||
namespace: gitea
|
||||
spec:
|
||||
provider:
|
||||
gcpsm:
|
||||
projectID: neuron-785695
|
||||
# Workload Identity is used automatically — no explicit auth needed
|
||||
# when the pod's ServiceAccount has the iam.gke.io/gcp-service-account annotation.
|
||||
---
|
||||
# gitea-db — Gitea database password, pulled from GCP Secret Manager.
|
||||
# The Secret Manager secret "gitea-database-url" stores the full DSN, but we
|
||||
# extract just the password field for use in GITEA__database__PASSWD.
|
||||
#
|
||||
# The full DSN format from Terraform:
|
||||
# host=/cloudsql/<conn> user=gitea password=<pw> dbname=gitea sslmode=disable
|
||||
#
|
||||
# ESO extracts the raw secret value. Since GCP Secret Manager stores the full
|
||||
# DSN as a single string, we store the password separately as "gitea-db-password"
|
||||
# so Gitea can receive it as a discrete env var.
|
||||
#
|
||||
# Bootstrap: after `terraform apply`, run:
|
||||
# PASSWORD=$(gcloud secrets versions access latest --secret=gitea-database-url \
|
||||
# | grep -oP '(?<=password=)\S+')
|
||||
# echo -n "$PASSWORD" | gcloud secrets create gitea-db-password \
|
||||
# --data-file=- --project=neuron-785695
|
||||
#
|
||||
# Or simpler — let Terraform write it directly. The gitea-db-password secret
|
||||
# is managed by the gitea-database-url secret version output. Use the full DSN
|
||||
# secret and parse in-pod, or store password separately.
|
||||
#
|
||||
# For simplicity: pull the full DSN and use it as GITEA__database__PASSWD
|
||||
# is wrong (it's a DSN, not a password). Instead, use the Cloud SQL proxy
|
||||
# unix socket and no password — configure Gitea to use peer auth.
|
||||
#
|
||||
# ACTUAL APPROACH: ExternalSecret pulls the full DSN string into a k8s Secret
|
||||
# key "dsn". A separate gitea-db secret provides just the password field.
|
||||
# Terraform outputs both; add a gitea-db-password Secret Manager secret in cloud-sql.tf.
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: gitea-db
|
||||
namespace: gitea
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: gcp-secretmanager
|
||||
kind: SecretStore
|
||||
target:
|
||||
name: gitea-db
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: password
|
||||
remoteRef:
|
||||
# This secret is populated by Terraform (gitea-db-password in cloud-sql.tf)
|
||||
# It contains just the raw database password (no DSN prefix).
|
||||
key: gitea-db-password
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: gitea
|
||||
labels:
|
||||
app.kubernetes.io/name: gitea
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: gitea-data
|
||||
namespace: gitea
|
||||
spec:
|
||||
# standard-rwo = pd-balanced on GKE Autopilot (ReadWriteOnce)
|
||||
# Use premium-rwo (pd-ssd) if repo performance becomes a bottleneck.
|
||||
storageClassName: standard-rwo
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
annotations:
|
||||
# Workload Identity — allows the Cloud SQL Auth Proxy sidecar to authenticate
|
||||
# to Cloud SQL as the gitea GCP SA without a JSON key file.
|
||||
# The GCP SA binding is in servers/gcp/gke.tf (gitea_workload_identity).
|
||||
iam.gke.io/gcp-service-account: gitea@neuron-785695.iam.gserviceaccount.com
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: vault
|
||||
labels:
|
||||
app.kubernetes.io/name: vault
|
||||
@@ -1,3 +1,12 @@
|
||||
# PENDING DECOMMISSION — migrating to GKE Autopilot (gke.tf). Remove after Vault data migrated.
|
||||
# Migration checklist:
|
||||
# 1. Bring up Vault StatefulSet on GKE (servers/gcp/k8s/vault/)
|
||||
# 2. Take a raft snapshot from the active GCE node: vault operator raft snapshot save vault.snap
|
||||
# 3. Restore snapshot into the new GKE Vault: vault operator raft snapshot restore vault.snap
|
||||
# 4. Validate all secrets are accessible from the GKE cluster
|
||||
# 5. Update Cloudflare DNS / LB to point vault.neuralplatform.ai at GKE ingress
|
||||
# 6. Remove this file and the vault-kms.tf GCE-specific IAM (keep the KMS key + SA)
|
||||
#
|
||||
# ── Vault HA Cluster — GCE-based Raft ────────────────────────────────────────
|
||||
#
|
||||
# Three GCE e2-small VMs across us-central1-{a,b,c} running HashiCorp Vault
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: gke-apps
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git
|
||||
targetRevision: main
|
||||
path: servers/gcp/k8s/argocd-apps
|
||||
destination:
|
||||
# This Application lives on Legion Argo CD — it creates child Applications
|
||||
# there that target the GKE cluster.
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
@@ -95,5 +95,18 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
# We deliberately don't set ENTRYPOINT / CMD here — act_runner spawns
|
||||
# build containers with its own entrypoint to keep them alive between
|
||||
# steps, and overriding it breaks job execution.
|
||||
|
||||
# In-cluster git redirect — GKE runners resolve the internal Gitea service
|
||||
# directly, bypassing Cloudflare Access. This insteadOf rule rewrites any
|
||||
# clone/fetch of the public URL to the in-cluster service URL so CI jobs work
|
||||
# without CF Access headers inside the GKE cluster.
|
||||
#
|
||||
# On Legion runners this is overridden at runtime by git-cf-access-init.sh
|
||||
# (which sets the reverse redirect + CF Access headers). The system-level rule
|
||||
# here is a safe baseline; the init script wins because it runs after.
|
||||
RUN git config --system \
|
||||
url."http://gitea.gitea.svc.cluster.local:3000/".insteadOf \
|
||||
"https://git.neuralplatform.ai/"
|
||||
|
||||
COPY git-cf-access-init.sh /usr/local/bin/git-cf-access-init.sh
|
||||
RUN chmod +x /usr/local/bin/git-cf-access-init.sh
|
||||
|
||||
Reference in New Issue
Block a user