~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
202 lines · 7.6 KB Raw History

01 - The kcp server

End state: a host running the socialweb-computer gateway, one tenant cluster at Ready, nodes sized for the job, and a runner credential in hand. Nothing here needs GitLab.

Read README.md first: the tenant apiserver binds loopback on this host, so GitLab has to run here.

1. Run the gateway

The gateway is the socialweb-computer binary under its own lingering account (socialweb on the production host). Install it with scripts/deploy.sh:

BINARY=<path to socialweb-computer> SHA256=<sha256> SSH_KEY=~/.ssh/id_ed25519_no_password \
  GATEWAY_ENV_FILE=~/.tmp/deploy/gateway.env GATEWAY_SPEC_FILE=~/.tmp/deploy/gateway-spec.yaml \
  scripts/deploy.sh

deploy.sh runs socialweb-computer install gateway the first time and upgrade after, then waits until the gateway answers /api/config. Check it as the account:

systemctl --user status socialweb-computer-gateway.service
curl -fsS http://127.0.0.1:8788/api/config

2. The tenant cluster

The cluster exists before its first node does. That is the point of the design: a cluster is a control plane, and nodes are rented when a Pod needs one.

kubectl --kubeconfig <admin> get clusters
kubectl --kubeconfig <admin> get cluster <name> -o jsonpath='{.status.url}'

<admin> is the kcp admin kubeconfig, named in the adminKubeconfig field of the gateway's gateway.json. That URL is https://127.0.0.1:<leased port>, and the port is per cluster. It is what goes in the runner's host, and what the runner's certificate is signed against. The admin kubeconfig never leaves the provider's hands and is not what a runner gets.

3. Size the nodes

A node boots at 2 vCPU and 2048 MiB. That fits a probe, not a toolchain. These numbers live in the guest image the bidder boots, recorded in its manifest as vcpu and memMiB, so changing either builds a new image.

The root filesystem fills before the disk

A job's image and its build output go in the node's ROOT filesystem, where containerd stores layers. Too small and the image pull dies:

failed to pull and unpack image ... no space left on device

That is the ROOT filesystem filling, and it arrives as a Pod that never starts. Size the image's root filesystem for the toolchain and its layers together. A large file staged INTO the image does not help: mkfs.ext4 -d refuses a file of about 2 GiB or more with Ext2 file too big. Anything that large is pulled by the guest at run time instead, see 03.

The state directory has to be on real disk

Node disks and root filesystems are large files. Where /tmp is a tmpfs that is RAM, point the state directory at the host's real disk:

TMPDIR=/var/tmp/kcp

A state directory that cannot hold a node's disks fails as a full filesystem partway through a boot, not as a configuration error.

Check the host before booting anything

socialweb-computer check-host -role gateway -state-dir <state dir>

It prints one line per check with its fix and exits 1 when any check fails (docs/host-contract.md).

4. Mint the runner's credential

A runner authenticates to the tenant apiserver with a CLIENT CERTIFICATE signed by that cluster's ClientCA. The credential is valid only against that cluster.

Do not use the superuser. system:admin in system:masters is cluster-admin on the tenant cluster and makes every RBAC rule below decorative.

SOCIALWEB_TENANTCREDENTIAL_STATE_DIR=/var/lib/socialweb \
SOCIALWEB_TENANTCREDENTIAL_CLUSTER=ci \
SOCIALWEB_TENANTCREDENTIAL_SERVER="$(kubectl --kubeconfig <admin> get cluster ci -o jsonpath='{.status.url}')" \
SOCIALWEB_TENANTCREDENTIAL_COMMON_NAME=gitlab-runner \
SOCIALWEB_TENANTCREDENTIAL_ORGANIZATIONS=gitlab-runner \
SOCIALWEB_TENANTCREDENTIAL_OUT_DIR=/etc/gitlab-runner/tenant-ci \
SOCIALWEB_TENANTCREDENTIAL_VALID_FOR=720h \
  socialweb-computer internal tenantcredential

It writes three files at mode 0600:

FileWhat
ca.crtthe cluster's serving CA, what the apiserver presents at that URL
tls.crtthe runner's client cert; CommonName is the username, Organizations are the groups
tls.keythe private key

VALID_FOR is optional and defaults to the mint's own validity. The credential has no revocation path, so its lifetime is how long a leaked copy is useful. To rotate, move the old files aside first: the tool refuses to overwrite.

A scoped identity can only come from the ClientCA on disk. The OpenBao-backed path's PKI role pins the organization to system:masters, so a refused COMMON_NAME or ORGANIZATIONS is that distinction and not a typo. In Go the scoped entry point is clusterprovider.MintTenantCredentialForClusterAs, which signs directly and so dodges the vault.

5. Permit that group in the tenant cluster

Apply against the TENANT cluster. The namespace first, because the runner does not create it.

kubectl --kubeconfig <tenant-admin> apply -f - <<'YAML'
apiVersion: v1
kind: Namespace
metadata:
  name: gitlab-runner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: gitlab-runner
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/exec", "pods/attach", "pods/log"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["secrets", "configmaps", "services", "serviceaccounts", "events", "persistentvolumeclaims"]
    verbs: ["create", "delete", "get", "list", "watch", "update"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["create", "delete", "get", "list", "update"]
  - apiGroups: ["policy"]
    resources: ["poddisruptionbudgets"]
    verbs: ["create", "delete", "get", "list", "update"]
  - apiGroups: ["scheduling.k8s.io"]
    resources: ["priorityclasses"]
    verbs: ["get", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: gitlab-runner
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gitlab-runner
subjects:
  - kind: Group
    name: gitlab-runner
    apiGroup: rbac.authorization.k8s.io
YAML

Group is the ORGANIZATIONS value from step 4. Change one, change the other. When they disagree, every job fails with a 403 that reads as a broken runner.

priorityclasses is cluster-scoped, so a namespaced Role cannot grant it. apps/deployments and scheduling.k8s.io/priorityclasses are only needed by the pre-warmed pause pods in 03.

Check both halves before blaming the runner:

openssl x509 -in /etc/gitlab-runner/tenant-ci/tls.crt -noout -subject
# subject=O = gitlab-runner, CN = gitlab-runner

kubectl --kubeconfig <tenant-admin> auth can-i create pods \
  --namespace gitlab-runner --as=gitlab-runner --as-group=gitlab-runner
# yes

Hand-back

What 02 needs from this document:

  • the apiserver URL, https://127.0.0.1:<leased port>
  • /etc/gitlab-runner/tenant-ci/{ca.crt,tls.crt,tls.key}
  • the group name, gitlab-runner
  • the namespace, gitlab-runner

GitLab on its own VM does not work

A GitLab on a separate host cannot reach the tenant apiserver, because the apiserver binds loopback on the kcp host. The runner reports connection timeouts, and no runner configuration fixes it.

Making it work means the kcp host publishing an authenticated front for the apiserver. That is a deliberate trust-boundary change, not a config step: the gateway refuses to bind anything but loopback on purpose, and the entrypoint publishes exactly one port whose only upstream is the gateway. Decide it deliberately.