Saltar a contenido

Cómo se despliega la plataforma

rendimiento despliega aplicaciones, pero no se despliega a sí mismo. La plataforma se instala con manifiestos simples de Kubernetes en deploy/, aplicados con kubectl apply -k deploy. Dejar la plataforma fuera de su propio control significa que un rendimiento descompuesto siempre se puede arreglar con kubectl.

Sus propios ajustes

Los manifiestos de deploy/ traen valores de ejemplo (example.com, registry.example.lan, [email protected]). Los ajustes de un clúster real van en una capa privada, una kustomization en un repositorio privado que se basa en deploy/ y reemplaza lo que es distinto:

# kustomization.yaml en un repositorio privado, junto a una copia de este
resources:
  - ../../rendimiento.ai/deploy
images:
  - name: registry.example.lan:5000/rendimiento
    newName: registry.home.lan:5000/rendimiento   # su registro
patches:
  - path: configmap.yaml          # el ConfigMap de rendimiento con sus ajustes
  - target: { kind: Ingress, name: rendimiento }
    patch: |-
      - { op: replace, path: /spec/rules/0/host, value: rendimiento.your-domain.com }
      - { op: replace, path: /spec/tls/0/hosts/0, value: rendimiento.your-domain.com }

Luego apunte el Makefile hacia ella desde un local.mk (ignorado por git) en la raíz del repositorio:

IMAGE := registry.home.lan:5000/rendimiento
DEPLOY_DIR := $(HOME)/private-repo/rendimiento-platform
TEST_EXCLUDE_NODES := my-gpu-node     # nodos que las pruebas remotas deben evitar
ITEST_REGISTRY := registry.home.lan:5000

make deploy genera DEPLOY_DIR (deploy/ de forma predeterminada) y lo aplica. Así sus nombres de host, nombres de nodos y correo nunca entran al repositorio público.

Qué se instala

flowchart TB
    subgraph rs[espacio de nombres rendimiento-system]
        dep[Deployment rendimiento<br/>1 réplica, Recreate]
        svc[Service rendimiento :80]
        ing[Ingress rendimiento.joserod.space<br/>TLS con cert-manager]
        db[(StatefulSet rendimiento-db<br/>Postgres 17, 5Gi en Longhorn)]
        cm[ConfigMap rendimiento<br/>ajustes]
        sa[ServiceAccount rendimiento]
        sa2[ServiceAccount rendimiento-addons<br/>sin pods, suplantada]
    end
    subgraph rb[espacio de nombres rendimiento-builds]
        np[NetworkPolicy isolate-builds]
    end
    crds[CRD apps.rendimiento.ai<br/>addons.rendimiento.ai]
    ing --> svc --> dep --> db
    dep -. lee .-> cm
Archivo Qué contiene
kustomization.yaml La lista de abajo, aplicada junta.
namespace.yaml rendimiento-system (la plataforma) y rendimiento-builds (los pods de integración continua).
crds/rendimiento.ai_apps.yaml, crds/rendimiento.ai_addons.yaml Las dos definiciones de recursos personalizados, generadas desde api/v1alpha1 con make generate. Nunca las edite a mano.
rbac.yaml Lo que puede hacer la plataforma (vea abajo), la identidad rendimiento-addons ligada a cluster-admin y el Role del espacio de nombres de construcción.
postgres.yaml La base de datos de la plataforma: un StatefulSet de una réplica sobre un volumen de Longhorn de 5 Gi.
rendimiento.yaml El ConfigMap de ajustes, el Deployment, su Service y su Ingress.
networkpolicy.yaml El aislamiento de los pods de integración continua: solo DNS, BuildKit e internet público.
railpack/Dockerfile No se aplica: la imagen con la herramienta de Railpack que usan los pods de construcción (make railpack-image).
deploy/rendimiento.yaml (ajustes, Deployment, Service, Ingress)
apiVersion: v1
kind: ConfigMap
metadata:
  name: rendimiento
  namespace: rendimiento-system
data:
  # Example values: a real cluster's settings live outside this public
  # repository (an overlay applied over these manifests; see the book).
  BASE_URL: https://rendimiento.example.com
  ALLOWED_USERS: your-github-login
  GITHUB_APP_NAME: rendimiento
  DNS_ZONE: example.com
  DNS_PROXIED: "true"
  REGISTRY: registry.example.lan:5000
  REGISTRY_INSECURE: "true"
  BUILDKIT_ADDR: tcp://buildkitd.devops-tools.svc.cluster.local:1234
  # One BuildKit per worker (the "buildkit" add-on); each image builds on
  # the daemon its name hashes to. BUILDKIT_ADDR is the fallback.
  BUILDKIT_POOL: buildkitd-pool.devops-tools.svc.cluster.local
  MAX_PARALLEL_STEPS: "4"
  # Longest a single test or build step may run. First builds on the Pis
  # start with a cold cache (e.g. compiling a Python wheel from source).
  STEP_TIMEOUT: 45m
  # Nodes that must never run builds: e.g. a node whose kernel cannot
  # enforce the build NetworkPolicy, or a GPU node reserved for inference.
  BUILD_EXCLUDE_NODES: gpu-node
  # One replica with the Recreate strategy never overlaps, so leader
  # election only adds a way to crash when the API server is slow.
  LEADER_ELECTION: "false"
  CLUSTER_ISSUER: letsencrypt-prod
  INGRESS_CLASS: nginx
  STORAGE_CLASS: longhorn
  # Labels added to every app volume claim; these put it in a Longhorn backup job.
  # VOLUME_LABELS: recurring-job.longhorn.io/source=enabled,recurring-job-group.longhorn.io/backup-nightly=enabled
  # Services without a Dockerfile are built from source with Railpack. The
  # CLI image comes from `make railpack-image`; keep both versions in step.
  RAILPACK_IMAGE: registry.example.lan:5000/rendimiento-railpack:0.40.0
  RAILPACK_FRONTEND: ghcr.io/railwayapp/railpack-frontend:v0.40.0
  # How a service with `gpu: 1` gets a GPU (here: an NVIDIA device plugin): its
  # runtime class, the host's driver libraries (read-only) and a larger /dev/shm.
  GPU_RESOURCE: nvidia.com/gpu
  GPU_RUNTIME_CLASS: nvidia
  GPU_HOST_PATHS: /usr/lib/aarch64-linux-gnu/nvidia
  GPU_ENV: NVIDIA_VISIBLE_DEVICES=all;NVIDIA_DRIVER_CAPABILITIES=all;LD_LIBRARY_PATH=/usr/lib/aarch64-linux-gnu/nvidia:/usr/local/cuda/lib64
  GPU_SHARED_MEMORY: 1Gi
  # Failed builds, rolled-back releases, outages and recoveries are emailed
  # here, through Resend (RESEND_API_KEY in the rendimiento-notify secret).
  NOTIFY_EMAIL_TO: [email protected]
  NOTIFY_LANG: en                # en | es (Mexican Spanish)
  NOTIFY_EMAIL_FROM: rendimiento <[email protected]>
  # GET /api/public/stats: aggregate numbers for a public page (a portfolio,
  # a status page), served only on the internal port 8081 (the Service's
  # "stats" port, which the public ingress does not route), so it is
  # reachable from inside the cluster but not from the internet.
  PUBLIC_STATS: "true"
  STATS_LISTEN: ":8081"
  PUBLIC_STATS_TZ: America/New_York
  # Finished runs' step logs move to S3-compatible storage (gzip; Garage
  # here), which deletes them after LOG_RETENTION_DAYS. Credentials: the
  # rendimiento-logs secret (a key that can only use this bucket).
  LOG_ARCHIVE_ENDPOINT: garage.garage.svc.cluster.local:3900
  LOG_ARCHIVE_BUCKET: rendimiento-logs
  LOG_RETENTION_DAYS: "365"
  # Visitor numbers from Umami (each app's Visits tab), read inside the
  # cluster with a view-only user (the rendimiento-umami secret).
  # UMAMI_URL: http://umami.umami.svc.cluster.local:3000
  # UMAMI_PUBLIC_URL: https://umami.example.com
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rendimiento
  namespace: rendimiento-system
spec:
  replicas: 1
  strategy: { type: Recreate }
  selector:
    matchLabels: { app: rendimiento }
  template:
    metadata:
      labels: { app: rendimiento }
    spec:
      serviceAccountName: rendimiento
      containers:
        - name: rendimiento
          image: registry.example.lan:5000/rendimiento:latest
          imagePullPolicy: Always
          envFrom:
            - configMapRef: { name: rendimiento }
            # CLOUDFLARE_API_TOKEN and DNS_TARGET; optional until DNS automation is wanted.
            - secretRef: { name: rendimiento-dns, optional: true }
            # RESEND_API_KEY for notification emails; optional (no email without it).
            - secretRef: { name: rendimiento-notify, optional: true }
            # LOG_ARCHIVE_ACCESS_KEY / LOG_ARCHIVE_SECRET_KEY for the log archive.
            - secretRef: { name: rendimiento-logs, optional: true }
            # UMAMI_USERNAME / UMAMI_PASSWORD (a view-only Umami user) for visitor numbers.
            - secretRef: { name: rendimiento-umami, optional: true }
          env:
            - name: DB_PASSWORD
              valueFrom: { secretKeyRef: { name: rendimiento-db, key: password } }
            - name: DATABASE_URL
              value: postgres://rendimiento:$(DB_PASSWORD)@rendimiento-db:5432/rendimiento?sslmode=disable
            - name: SETUP_TOKEN
              valueFrom: { secretKeyRef: { name: rendimiento-setup, key: token } }
          ports:
            - { name: http, containerPort: 8080 }
            - { name: metrics, containerPort: 9090 }
            - { name: stats, containerPort: 8081 }
          readinessProbe:
            httpGet: { path: /healthz, port: http }
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /healthz, port: http }
            initialDelaySeconds: 20
            periodSeconds: 20
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { memory: 512Mi }
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            runAsUser: 65532
            runAsGroup: 65532
            capabilities: { drop: [ALL] }
---
apiVersion: v1
kind: Service
metadata:
  name: rendimiento
  namespace: rendimiento-system
spec:
  selector: { app: rendimiento }
  ports:
    - { name: http, port: 80, targetPort: http }
    # In-cluster only: the public stats (the Ingress routes only "http").
    - { name: stats, port: 8081, targetPort: stats }
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rendimiento
  namespace: rendimiento-system
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    # Live logs use server-sent events.
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-body-size: 25m
spec:
  ingressClassName: nginx
  tls:
    - hosts: [rendimiento.example.com]
      secretName: rendimiento-tls
  rules:
    - host: rendimiento.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: rendimiento, port: { name: http } }
deploy/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rendimiento
  namespace: rendimiento-system
---
# Cluster-wide because each app gets its own namespace. The controller
# refuses namespaces and hostnames it does not own (see checkOwnership).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: rendimiento
rules:
  - apiGroups: [rendimiento.ai]
    resources: [apps, apps/status, apps/finalizers, addons, addons/status, addons/finalizers]
    verbs: ["*"]
  # Add-ons are applied as the rendimiento-addons identity (below); the
  # platform itself may only act as it, not hold its rights.
  - apiGroups: [""]
    resources: [serviceaccounts]
    resourceNames: [rendimiento-addons]
    verbs: [impersonate]
  - apiGroups: [""]
    resources: [namespaces, services, persistentvolumeclaims, secrets]
    verbs: [get, list, watch, create, update, patch, delete]
  - apiGroups: [""]
    resources: [pods]
    verbs: [get, list, watch]
  - apiGroups: [apps]
    resources: [deployments]
    verbs: [get, list, watch, create, update, patch, delete]
  - apiGroups: [networking.k8s.io]
    resources: [ingresses]
    verbs: [get, list, watch, create, update, patch, delete]
  - apiGroups: [batch]
    resources: [cronjobs]
    verbs: [get, list, watch, create, update, patch, delete]
  # Post-deploy tasks run as Jobs in the app's namespace; their logs are kept.
  - apiGroups: [batch]
    resources: [jobs]
    verbs: [get, list, watch, create, delete]
  - apiGroups: [""]
    resources: [pods/log]
    verbs: [get]
  - apiGroups: [cert-manager.io]
    resources: [certificates]
    verbs: [get, list, watch]
  - apiGroups: [""]
    resources: [events]
    verbs: [create, patch]
  # Read-only, for the environment page.
  - apiGroups: [""]
    resources: [nodes]
    verbs: [get, list]
  - apiGroups: [metrics.k8s.io]
    resources: [nodes]
    verbs: [get, list]
  - apiGroups: [networking.k8s.io]
    resources: [ingressclasses, networkpolicies]
    verbs: [get, list]
  - apiGroups: [storage.k8s.io]
    resources: [storageclasses]
    verbs: [get, list]
  - apiGroups: [apiextensions.k8s.io]
    resources: [customresourcedefinitions]
    verbs: [get, list]
  - apiGroups: [cert-manager.io]
    resources: [clusterissuers]
    verbs: [get, list]
  - apiGroups: [longhorn.io]
    resources: [volumes, recurringjobs, backuptargets]
    verbs: [get, list]
  # Read-only, for the services catalog (who calls what).
  - apiGroups: [apps]
    resources: [statefulsets]
    verbs: [get, list]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: rendimiento
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: rendimiento
subjects:
  - kind: ServiceAccount
    name: rendimiento
    namespace: rendimiento-system
---
# CI pods: create, watch, read logs, clean up.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: rendimiento-builds
  namespace: rendimiento-builds
rules:
  - apiGroups: [""]
    resources: [pods, secrets]
    verbs: [get, list, watch, create, delete]
  - apiGroups: [""]
    resources: [pods/log]
    verbs: [get]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: rendimiento-builds
  namespace: rendimiento-builds
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rendimiento-builds
subjects:
  - kind: ServiceAccount
    name: rendimiento
    namespace: rendimiento-system
---
# Leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: rendimiento-leader
  namespace: rendimiento-system
rules:
  - apiGroups: [coordination.k8s.io]
    resources: [leases]
    verbs: [get, list, watch, create, update, patch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: rendimiento-leader
  namespace: rendimiento-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rendimiento-leader
subjects:
  - kind: ServiceAccount
    name: rendimiento
    namespace: rendimiento-system
---
# Add-ons install cluster software (Helm charts such as Longhorn: CRDs,
# ClusterRoles, DaemonSets), which needs cluster-admin, as ArgoCD has had.
# No pod runs as this account: the platform impersonates it only while
# applying add-ons, so the audit log shows exactly what add-ons changed.
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rendimiento-addons
  namespace: rendimiento-system
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: rendimiento-addons
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: rendimiento-addons
    namespace: rendimiento-system
deploy/networkpolicy.yaml
# CI steps run code from the repos being built (tests, Dockerfile RUN lines).
# Keep them away from everything else: they may reach DNS, the shared
# BuildKit daemon and the public internet (git, package registries), but
# not other apps, databases, the Kubernetes API or the home network.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: isolate-builds
  namespace: rendimiento-builds
  labels:
    app.kubernetes.io/managed-by: rendimiento
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
  ingress: []   # nothing connects to build pods
  egress:
    - to:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
          podSelector:
            matchLabels: { k8s-app: kube-dns }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }
    - to:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: devops-tools }
          podSelector:
            matchLabels: { app: buildkitd }
      ports:
        - { protocol: TCP, port: 1234 }
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8       # cluster pods and services (10.42/16, 10.43/16)
              - 172.16.0.0/12
              - 192.168.0.0/16   # home network, including the router
              - 169.254.0.0/16   # link-local / cloud metadata
              - 100.64.0.0/10    # carrier-grade NAT

Los permisos, y por qué tienen esta forma

La cuenta de servicio propia de la plataforma (rendimiento) puede:

  • administrar los recursos App y Addon;
  • administrar espacios de nombres, Deployments, Services, Ingresses, CronJobs, PVCs y Secrets en todo el clúster, porque cada aplicación recibe su propio espacio de nombres. El controlador rechaza los espacios de nombres y nombres de host que no le pertenecen (comprobaciones de propiedad);
  • leer nodos, métricas, clases de entrada, clases de almacenamiento, CRD, emisores del clúster y StatefulSets, para las páginas Entorno y Servicios;
  • crear y borrar pods y secretos en rendimiento-builds (integración continua);
  • suplantar a la cuenta de servicio rendimiento-addons, y nada más.

rendimiento-addons está ligada a cluster-admin, porque instalar programas como Longhorn significa crear CRD, ClusterRoles y DaemonSets, exactamente lo que necesitaba ArgoCD. Ningún pod corre con ella: solo el controlador de complementos actúa como ella, así la bitácora de auditoría de Kubernetes muestra cada cambio de complemento bajo ese nombre. Vea Modelo de seguridad.

Los secretos que se crean una vez

Secret (espacio de nombres rendimiento-system) Llaves Se usa para
rendimiento-db password La contraseña de Postgres (el Deployment arma DATABASE_URL con ella)
rendimiento-setup token El token de un solo uso que protege la página de configuración de la aplicación de GitHub
rendimiento-dns (opcional) CLOUDFLARE_API_TOKEN, DNS_TARGET La automatización de DNS. DNS_TARGET=auto sigue la IP pública de la red doméstica.
rendimiento-github lo crea el flujo de configuración El ID, la llave privada, el secreto de avisos web y el cliente OAuth de la aplicación de GitHub
kubectl -n rendimiento-system create secret generic rendimiento-db --from-literal=password="$(openssl rand -hex 24)"
kubectl -n rendimiento-system create secret generic rendimiento-setup --from-literal=token="$(openssl rand -hex 16)"
# un token de Cloudflare con Zone:DNS:Edit en sus zonas, escrito sin mostrarlo en pantalla:
read -rs CF && kubectl -n rendimiento-system create secret generic rendimiento-dns \
  --from-literal=CLOUDFLARE_API_TOKEN="$CF" --from-literal=DNS_TARGET=auto; unset CF

La aplicación de GitHub

rendimiento crea su propia aplicación de GitHub con el flujo de manifiesto, así nadie llena a mano los formularios de GitHub:

sequenceDiagram
    actor You as Usted
    participant R as rendimiento
    participant G as GitHub
    You->>R: abre /api/setup/github?token=… (el token de rendimiento-setup)
    R->>You: una página que manda el manifiesto de la aplicación a GitHub
    You->>G: crea la aplicación (nombre, permisos, URL de avisos web)
    G->>R: redirige a /api/setup/github/callback?code=…
    R->>G: cambia el código por las credenciales de la aplicación
    R->>R: las guarda en el secreto rendimiento-github
    You->>G: instala la aplicación en su cuenta (todos o algunos repositorios)

El manifiesto pide estos permisos de repositorio: Contents de lectura y escritura (leer el código, enviar las ramas de incorporación), Pull requests de lectura y escritura (abrir las solicitudes de incorporación), Checks de lectura y escritura (reportar el estado de la integración continua), Metadata de lectura, Issues de lectura y escritura y Commit statuses de lectura (estos dos para el complemento de Renovate). Se suscribe a los eventos de envío (push). El inicio de sesión en el panel usa el OAuth de la misma aplicación, y solo los usuarios de ALLOWED_USERS reciben una sesión.

Cambiar después los permisos de la aplicación

Cámbielos en https://github.com/settings/apps/<app-name>/permissions y luego acepte los permisos nuevos en la instalación (https://github.com/settings/installations → Configure). Mientras no se acepten, las instalaciones conservan los anteriores; la página Complementos avisa cuando a Renovate le falta lo que necesita.

Publicar una versión nueva de rendimiento

make test-remote     # todas las pruebas, en un nodo trabajador
make image           # construye y envía registry.example.lan:5000/rendimiento:latest en el grupo de BuildKit
make deploy          # kubectl apply -k deploy (solo hace falta si cambiaron deploy/ o los CRD)
kubectl -n rendimiento-system rollout restart deploy/rendimiento
kubectl -n rendimiento-system rollout status deploy/rendimiento

El Deployment descarga :latest con imagePullPolicy: Always y usa la estrategia Recreate (una réplica, nunca dos a la vez), así que un reinicio tarda unos 20 segundos durante los cuales la interfaz y los avisos web no están disponibles. Las aplicaciones siguen corriendo: no dependen de que la plataforma esté arriba. GitHub reintenta los avisos web que fallan.

Al reiniciar, la plataforma:

  1. aplica las migraciones de la base de datos (internal/store/migrations), cada una una sola vez;
  2. vuelve a poner en cola las ejecuciones de integración continua que un proceso anterior dejó corriendo (hasta dos reintentos) y borra los pods de construcción sobrantes;
  3. cierra las ejecuciones de Renovate que se interrumpieron;
  4. arranca los controladores, el trabajador de integración continua, el programador de Renovate, la sincronización de complementos desde git y el DNS dinámico.

:latest no tiene historial

Revertir la plataforma significa volver a construir la confirmación anterior. Etiquetar cada construcción con su confirmación está en la hoja de ruta; mientras tanto, git checkout <confirmación buena> && make image es la reversión.

rendimiento se construye a sí mismo

El repositorio de rendimiento está incorporado como cualquier aplicación. Su rendimiento.yaml tiene el libro (docs, un servicio) y la imagen de la plataforma (builds: platform), así que cada envío corre:

  • platform:test: hack/ci-test.sh (generar, vet y las pruebas de Go, con envtest y un Postgres desechable) en golang, con una caché que se conserva;
  • platform:build: el Dockerfile, que además revisa los tipos y las traducciones de la interfaz, publicado como <registro>/rendimiento-ai-platform:<confirmación>.

Las solicitudes de incorporación reciben la misma comprobación, sin versión. En la rama principal, el resumen (digest) de la imagen se guarda con la versión, junto al del libro. Todavía no se despliega solo: publicar sigue siendo los pasos de arriba, que construyen el mismo Dockerfile. El siguiente paso es que una versión actualice la plataforma (con una actualización gradual que conserve la versión anterior hasta que la nueva esté lista, para que una imagen rota no pueda tumbar la plataforma).

Un campo que la plataforma en marcha no conoce

La plataforma lee rendimiento.yaml de forma estricta. Un cambio que le agrega un campo (como builds:) debe publicarse antes de enviar la confirmación que lo usa; si no, la ejecución de ese envío no puede leer su propio spec.

El lado de las construcciones

Qué De dónde viene
Demonios de BuildKit el complemento buildkit (p0dxD/gitops/buildkit/): un StatefulSet con un demonio por nodo trabajador
Imagen de la herramienta de Railpack make railpack-image desde deploy/railpack/Dockerfile, fijada por suma de verificación
Interfaz de entrada de Railpack BuildKit la descarga de ghcr.io/railwayapp/railpack-frontend en la versión fijada
Imágenes de clonado y del cliente de construcción alpine/git, moby/buildkit (el cliente debe coincidir con la versión del demonio)