deploy gitea CI runner to GKE as k8s pod

Replaces the GCE VM runner with a k8s Deployment in the ci namespace on
neuron-platform GKE. Uses Docker-in-Docker for build isolation since
Autopilot doesn't expose the node socket. Runner token pulled from Secret
Manager via ESO + Workload Identity.

- servers/gcp/k8s/gitea-runner/: namespace, serviceaccount, external-secrets,
  deployment manifests (ci namespace, dind sidecar, idempotent registration)
- servers/gcp/k8s/argocd-apps/gitea-runner-gke.yaml: Argo CD Application
- servers/gcp/gitea-runner.tf: gitea-runner-gke GCP SA with secretAccessor
  on gitea-runner-token, Workload Identity binding for ci/gitea-runner,
  artifactregistry.reader for pulling ci-base image
This commit is contained in:
Will Anderson
2026-05-05 10:16:49 -05:00
parent 4dc687f2ae
commit c31edc8b83
6 changed files with 245 additions and 0 deletions
+40
View File
@@ -254,6 +254,46 @@ resource "google_service_account_iam_member" "ci_pusher_wif_neuron_web" {
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.gitea.name}/attribute.repository/neuron-technologies/neuron-web"
}
# ── Gitea Actions runner on GKE ───────────────────────────────────────────────
# A k8s Deployment in the `ci` namespace on GKE runs act_runner with a DinD
# sidecar. The pod uses this GCP SA via Workload Identity to pull the runner
# registration token from Secret Manager at startup.
#
# See servers/gcp/k8s/gitea-runner/ for the k8s manifests.
resource "google_service_account" "gitea_runner_gke" {
account_id = "gitea-runner-gke"
display_name = "Gitea Actions runner (GKE identity)"
description = "Workload Identity SA for the GKE-hosted Gitea Actions runner. Read-only access to the registration token."
project = var.project_id
}
# Allow the GKE SA to read the runner registration token from Secret Manager.
resource "google_secret_manager_secret_iam_member" "runner_gke_token_access" {
project = var.project_id
secret_id = "gitea-runner-token"
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.gitea_runner_gke.email}"
}
# Workload Identity binding — the k8s SA `gitea-runner` in the `ci` namespace
# can impersonate this GCP SA without a JSON key file.
resource "google_service_account_iam_member" "gitea_runner_gke_workload_identity" {
service_account_id = google_service_account.gitea_runner_gke.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[ci/gitea-runner]"
depends_on = [google_container_cluster.neuron_platform]
}
# Allow the runner to pull images from Artifact Registry (needed for build jobs
# that pull from neuron-* repos, and for the runner's own ci-base image).
resource "google_project_iam_member" "gitea_runner_gke_ar_reader" {
project = var.project_id
role = "roles/artifactregistry.reader"
member = "serviceAccount:${google_service_account.gitea_runner_gke.email}"
}
# ── Outputs ───────────────────────────────────────────────────────────────────
output "gitea_runner_vm_name" {
@@ -0,0 +1,26 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: gitea-runner-gke
namespace: argocd
spec:
project: default
source:
repoURL: https://git.neuralplatform.ai/will/infrastructure.git
targetRevision: main
path: servers/gcp/k8s/gitea-runner
destination:
server: https://34.63.89.52
namespace: ci
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /status
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
@@ -0,0 +1,121 @@
---
# Gitea Actions runner on GKE.
#
# Architecture:
# - init container registers the runner with Gitea (--no-interactive, idempotent)
# - main container runs act_runner daemon
# - docker:dind sidecar provides Docker for build jobs (privileged)
# - emptyDir /data persists runner state within a pod lifetime
#
# The runner uses Docker-in-Docker (DinD) rather than a host socket because
# GKE Autopilot does not expose the node's Docker socket.
# DinD requires privileged mode — the ci namespace has
# pod-security.kubernetes.io/enforce: privileged (see namespace.yaml).
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea-runner
namespace: ci
labels:
app: gitea-runner
spec:
replicas: 1
selector:
matchLabels:
app: gitea-runner
template:
metadata:
labels:
app: gitea-runner
spec:
serviceAccountName: gitea-runner
securityContext:
runAsNonRoot: false
initContainers:
- name: register
image: us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest
command: ["/bin/sh", "-c"]
args:
- |
# Idempotent registration — skip if already registered (.runner file exists).
if [ -f /data/.runner ]; then
echo "Runner already registered, skipping."
else
act_runner register \
--instance "${GITEA_INSTANCE_URL}" \
--token "${GITEA_RUNNER_REGISTRATION_TOKEN}" \
--name "gke-runner-$(hostname)" \
--labels "ubuntu-latest:docker://us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest,ubuntu-24.04:docker://us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest,linux,x64" \
--no-interactive
fi
cat > /data/config.yaml << 'CONFIGEOF'
runner:
capacity: 2
timeout: 3h
container:
network: host
docker_host: "tcp://localhost:2375"
force_pull: true
valid_volumes: []
default_image: "us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest"
env:
GITEA_SSH_PRIVATE_KEY: ""
BASH_ENV: "/usr/local/bin/git-ssh-init.sh"
CONFIGEOF
env:
- name: GITEA_INSTANCE_URL
value: "https://git.neuralplatform.ai"
- name: DOCKER_HOST
value: "tcp://localhost:2375"
envFrom:
- secretRef:
name: gitea-runner-token
volumeMounts:
- mountPath: /data
name: data
workingDir: /data
containers:
- name: runner
image: us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest
command: ["act_runner", "daemon", "--config", "/data/config.yaml"]
env:
- name: GITEA_INSTANCE_URL
value: "https://git.neuralplatform.ai"
- name: DOCKER_HOST
value: "tcp://localhost:2375"
envFrom:
- secretRef:
name: gitea-runner-token
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "4"
memory: "4Gi"
volumeMounts:
- mountPath: /data
name: data
workingDir: /data
- name: dind
image: docker:dind
securityContext:
privileged: true
env:
- name: DOCKER_TLS_CERTDIR
value: ""
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "4"
memory: "4Gi"
volumeMounts:
- mountPath: /var/lib/docker
name: docker-storage
volumes:
- name: data
emptyDir: {}
- name: docker-storage
emptyDir: {}
@@ -0,0 +1,39 @@
---
# SecretStore for the CI namespace — uses GCP Secret Manager via Workload Identity.
# The gitea-runner-gke GCP SA has secretmanager.secretAccessor on gitea-runner-token
# (see servers/gcp/gitea-runner.tf).
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: gcp-secretmanager
namespace: ci
spec:
provider:
gcpsm:
projectID: neuron-785695
auth:
workloadIdentity:
clusterLocation: us-central1
clusterName: neuron-platform
serviceAccountRef:
name: gitea-runner
namespace: ci
---
# Pull the Gitea runner registration token from Secret Manager into a k8s Secret.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: gitea-runner-token
namespace: ci
spec:
refreshInterval: 1h
secretStoreRef:
name: gcp-secretmanager
kind: SecretStore
target:
name: gitea-runner-token
creationPolicy: Owner
data:
- secretKey: GITEA_RUNNER_REGISTRATION_TOKEN
remoteRef:
key: gitea-runner-token
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Namespace
metadata:
name: ci
labels:
app.kubernetes.io/name: ci
# DinD (Docker-in-Docker) requires privileged pods.
pod-security.kubernetes.io/enforce: privileged
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitea-runner
namespace: ci
annotations:
# Workload Identity — allows ESO (and optionally the runner pod) to
# authenticate to GCP Secret Manager as the gitea-runner-gke GCP SA
# without a JSON key file.
# The GCP SA binding is in servers/gcp/gitea-runner.tf (gitea_runner_gke_workload_identity).
iam.gke.io/gcp-service-account: gitea-runner-gke@neuron-785695.iam.gserviceaccount.com