RAILSHOT.
Back to the hall

Railshot

AI deployment & operations platform

Deploy a locally built web app to prepared cloud and on-premises environments, then run it.

An AI deployment and operations platform that takes a web app built locally to prepared cloud or on-premises environments and keeps running it.

2026.09.28 — 2026.10.04 · Completed

Team project · my part: CI planning and implementation, CSP clusters and Ansible interface, operations, team code review and integration

SoftBank Hackathon 2026 powered by KOREC & ProgatePreliminary participant · finals selection — be announced 2026.10.26 (as of 2026.10.07)

  • WebImplemented · hackathon
  • MCP serverImplemented · remote · local

Operations and customer app clusters are separate, and the public path is kept apart from deployment control.

In the operations K3s, the dashboard, product API and build controller are placed on platform-role nodes and runner Jobs on a build-role node, while Argo CD handles deployment. Customer apps run in a designated namespace of a separate single-node K3s + Cilium cluster for each registered target.

Cluster map · drawn from the declarations and code at public commit 645e28a
Railshot operations and customer cluster mapThe operations K3s cluster and a registered customer app cluster. In the operations cluster, the railshot-system namespace places the dashboard, product API, remote MCP (only when its image is supplied) and build controller on platform-role nodes; runner Jobs in the railshot-build namespace take GitHub Actions jobs on the tainted build-role node. A GitHub-hosted release job publishes the tested bundle to GHCR. The product API commits the workload to config Git and syncs an Application in the argocd namespace, and Argo CD applies a Deployment, NodePort Service and NetworkPolicy to the namespace registered in the customer cluster. App requests enter through a provider L7 entry to the node’s NodePort and Service, then the app Pod. The deployment verdict rests only on Argo status and the public URL check; Pod diagnostics, logs and metrics are read as separate signals. Operator install and registration inputs are kept apart from the tenant app payload.Railshot userdashboard · MCPOperator bootstrapinstall · register · SecretGHCRdigest imagesconfig Git repositoryworkload.json · revisionProvider L7 entryALB · GCP LB · AmphoraApp userpublic HTTPSObserver VMoutside customer K3s · Composerailshot-systemdashboard · nginxremote MCP · only with an imagebuild controller · CronJobproduct API · cd.js → bridge.pyargocdArgo CD · ApplicationAppProject · cluster Secretrenewal CronJob · every 2 hrailshot-buildrunner Job · hostNetwork · Docker socketrailshot-deploy.ymlloop job · self-hosted runnerrelease job · GitHub-hosted<app namespace>Deployment · Pod · digest imageNodePort Service · LocalNetworkPolicy · listed CIDRsrailshot-argocd SA · tokenkube-systemCilium · Helm · VXLANrailshot-observabilitynode-exporterkube-state-metricsOperations cluster · K3sAWS · role nodesplatform-role nodesnodeSelectorbuild-role nodeNoSchedule taintGitHubActions · GHCRCustomer app cluster · exampleone per registered targetOne K3s node · serverCilium · kube-proxy kept1workflow_dispatch2replenish runner Job3take the job4tested bundle5 publish by digest · release job · GitHub-hosted → GHCR51workload commit · push2revision3 Application sync · product API · cd.js → bridge.py → Argo CD · Application34apply to namespace5pull by digest1HTTPS 4432NodePort3 to the Pod · NodePort Service · Local → Deployment · Pod · digest image34 enforce policy · Cilium · Helm · VXLAN → NetworkPolicy · listed CIDRs45 dashboard · API · Railshot user → dashboard · nginx51 revision · health · images · product API · cd.js → bridge.py → Argo CD · Application12public HTTPS check3Pod diagnostic · not the verdict4log tail · 100 lines per Pod5scrape · NodePorts6Blackbox probe1render platform · Secret2registration bootstrap3install K3s · Cilium4TokenRequest · 6 h

Build → publish image

The product API requests a GitHub Actions run, and the runner on the build node takes and checks the job. Only a bundle that passed the same checks is published to GHCR by a GitHub-hosted release job.

  1. product API · cd.js → bridge.pyrailshot-systemrun · apply: workflow_dispatchloop job · self-hosted runnerrailshot-deploy.yml

    The product API requests a GitHub Actions run with tenant, app, source commit and target. The API never calls a runner Job directly.

  2. build controller · CronJobrailshot-systemrun · apply: replenish runner Jobrunner Job · hostNetwork · Docker socketrailshot-build

    The build controller CronJob (every minute, on a platform node) creates runner Jobs in the railshot-build namespace. Its Role allows only Job get, list and create plus get, update and patch on the one runner-registration Secret; it cannot create namespaces or other Secrets.

  3. runner Job · hostNetwork · Docker socketrailshot-buildrun · apply: take the jobloop job · self-hosted runnerrailshot-deploy.yml

    The runner on the build-role node (exact hostname, NoSchedule toleration) takes the loop job by its self-hosted labels and runs it. Customer source runs in the existing restricted Docker build and check containers.

  4. runner Job · hostNetwork · Docker socketrailshot-builddata: tested bundlerelease job · GitHub-hostedrailshot-deploy.yml

    The loop job leaves the bundle that passed the same checks, and the release job takes only that bundle.

  5. release job · GitHub-hostedrailshot-deploy.ymldata: publish by digestGHCR

    A GitHub-hosted release job (ubuntu-24.04) publishes the tested bundle’s image to GHCR without rebuilding it. Image publishing never happens inside the customer app cluster.

Config Git → Argo → customer namespace

Config Git carries the desired resources and GHCR the image bytes. Argo CD applies only to the registered customer namespace.

  1. product API · cd.js → bridge.pyrailshot-systemdata: workload commit · pushconfig Git repository

    cd.js runs bridge.py, which commits and pushes workload.json only when the publication record and the registered target agree, then reads the remote revision back. Git carries the desired resources; GHCR carries the image bytes.

  2. config Git repositorydata: revisionArgo CD · Applicationargocd

    The Application reads only its restricted config repository, path and revision.

  3. product API · cd.js → bridge.pyrailshot-systemrun · apply: Application syncArgo CD · Applicationargocd

    The named Application is set by server-side apply, and a sync of one exact revision is requested without prune. The product API’s railshot-product ServiceAccount holds both the operator-installed registration bootstrap (create for Secrets, AppProjects and Applications in argocd; get, update and escalate on named Roles) and get and patch grants appended by exact name for each validated target. Kubernetes create cannot be limited by name, so the product’s input validation also keeps the names in scope.

  4. Argo CD · Applicationargocdrun · apply: apply to namespace<app namespace>

    Argo CD applies to the designated namespace in the customer cluster with the registered cluster Secret’s credential. The AppProject allows only namespaced kinds and no cluster-scoped resources.

  5. Deployment · Pod · digest image<app namespace>data: pull by digestGHCR

    The published digest image is pulled with a pull Secret referenced by name in the same namespace. Secret values never enter the artifact or config Git.

Public request path

App requests go provider L7 entry → node NodePort → Service → app Pod. Cilium’s policy enforcement is a separate control, not a hop that carries the request. The way into the operations dashboard is a separate path.

  1. App userdata: HTTPS 443Provider L7 entry

    App users reach the provider’s L7 entry by the registered hostname. Each provider implements its own entry.

  2. Provider L7 entrydata: NodePortNodePort Service · Local<app namespace>

    The entry forwards to the node’s private address and NodePort. The NodePort is the one allocated at target registration.

  3. NodePort Service · Local<app namespace>data: to the PodDeployment · Pod · digest image<app namespace>

    The Service sends the request to the app Pod (Deployment) on the same node. With externalTrafficPolicy Local, the source address reaches the NetworkPolicy’s registered-CIDR check unchanged.

  4. Cilium · Helm · VXLANkube-systemrun · apply: enforce policyNetworkPolicy · listed CIDRs<app namespace>

    Cilium enforces the NetworkPolicy: ingress only from the registered CIDRs to the app port; no egress by default, except an approved database address. Cloud firewalls and host rules still matter.

  5. Railshot userdata: dashboard · APIdashboard · nginxrailshot-system

    Users come through the operations entry to the nginx dashboard, and nginx fronts the product API. The dashboard Service is a NodePort only when an approved node port is given; the API stays ClusterIP.

Verify · observe

The deployment verdict rests only on Argo status and the public URL check. Pod diagnostics, current logs and operating metrics are separate signals that never change it.

  1. product API · cd.js → bridge.pyrailshot-systemobserve: revision · health · imagesArgo CD · Applicationargocd

    Deployed requires the Application’s sync revision, Synced and Healthy, operation Succeeded, and the image references recorded in the Argo Application status equal to the reviewed Deployment’s digest images. It is not a direct Pod imageID gate, and a green Argo status alone does not establish the match.

  2. product API · cd.js → bridge.pyrailshot-systemobserve: public HTTPS checkProvider L7 entry

    The registered public URL is checked without redirects. Succeeded is recorded only with both the Argo verdict and this check.

  3. product API · cd.js → bridge.pyrailshot-systemobserve: Pod diagnostic · not the verdictDeployment · Pod · digest image<app namespace>

    After the Argo verdict and the public check, the Deployment, ReplicaSets and Pods are read separately with the registered credential, and imageID and readiness are appended as a ready, progressing or unavailable diagnostic. A failed or differing diagnostic never changes the deployed or succeeded verdict.

  4. product API · cd.js → bridge.pyrailshot-systemobserve: log tail · 100 lines per PodDeployment · Pod · digest image<app namespace>

    Only for the currently deployed operation, with the trusted config and the registered credential. For each current Pod (at most three), the running image digest must equal the expected digest; then up to 100 redacted lines per Pod are read within a byte limit. An older attempt answers superseded; without configuration, not_configured. This is not log aggregation.

  5. Observer VMobserve: scrape · NodePortsrailshot-observability

    Prometheus on the observer VM scrapes node-exporter and kube-state-metrics through two firewall-restricted NodePorts. Missing or stale values are unknown, not zero.

  6. Observer VMobserve: Blackbox probeProvider L7 entry

    probe_success=1 means a 2xx at that probe location. It proves neither every user’s access nor the deployed version, and it never promotes published to deployed.

Operator install · register

Cluster installation, the registration bootstrap and target inputs, Secrets and the credential renewal setup are operator inputs, never part of the tenant app payload.

  1. Operator bootstraprun · apply: render platform · Secretrailshot-system

    The operator renders the platform declaration with render-platform.py and supplies the private operations config and Secrets. The remote MCP is included only when its image is supplied, the personal gateway (WireGuard) only with --personal.

  2. Operator bootstraprun · apply: registration bootstrapAppProject · cluster Secretargocd

    The operator installs the registration bootstrap Role and an empty registrations Role in argocd and prepares the target inputs. The registration helpers the product API runs (environment.py, applications.py) then write the railshot-<target> Secret with a scoped credential (clusterResources false, allowed namespaces listed) and append the validated exact names to the registrations Role. Per-app registration binds an app-<id> namespace to an AppProject of the same name.

  3. Operator bootstraprun · apply: install K3s · CiliumCilium · Helm · VXLANkube-system

    install-k3s.sh installs a single-node K3s server (flannel, network policy, traefik and servicelb off; kube-proxy kept), and Cilium is installed as a Helm release (kubeProxyReplacement false, VXLAN, Hubble off).

  4. renewal CronJob · every 2 hargocdrun · apply: TokenRequest · 6 hrailshot-argocd SA · token<app namespace>

    The railshot-credentials CronJob requests a fresh six-hour token for the customer SA every two hours. The customer-side Role allows only token create for its own SA. Six hours is one token’s TTL, not when operator access ends.

Every connection at the same weight. Pick a flow to highlight only its path, with numbered steps.

  • Solid · the direction requests, bundles, images and declarations flow
  • Dashed · run requests, apply, install, credentials
  • Long dash-dot · reading status, logs, probes, metrics; separate from the verdict
  • Heavy black line · the selected flow; numbers give the order
  • Box · a namespace, its name in the header (not a node)
  • Inner chip · a component in that namespace
  • Dashed box · outside the clusters
  • railshot-system · product
  • argocd · deployment authority
  • railshot-build · host Docker authority
  • GitHub · customer cluster

The October 2 placement decision in docs/architecture (two role nodes) and the public README diagram are history; this map follows the declarations and code at 645e28a. Arrows show cause and direction, not proof that every provider path is live now.

Namespaces and nodes
A namespace is a naming scope: neither hardware nor, by itself, a security boundary. The platform and build roles are scheduling placement by nodeSelector and taint; this map does not fix the operations cluster’s node count. Argo CD’s node placement is also a separate check.
Build-node authority
Runner Jobs run with hostNetwork, the host Docker socket, NET_ADMIN and root. A namespace, taint or RBAC does not remove host Docker authority, so this is neither VM-administrator isolation nor a sandbox for arbitrary code.
Customer namespace
Each registered target uses a non-default namespace and AppProject (for example tenant-demo, or app-<id> per app). Only one service’s Deployment (ServiceAccount token automount off), NodePort Service and NetworkPolicy, plus an optional PVC or migration Job, are created; Secrets are only referenced by name (the pull Secret and an approved DATABASE_URL), never created.
AWS entry
Route 53 Alias → shared ALB HTTPS 443 → customer node private address and NodePort. Many apps reuse one node, each with its own namespace, Application and NodePort.
GCP entry
Global EXTERNAL_MANAGED HTTPS LB → zonal GCE_VM_IP_PORT NEG → the existing VM’s private NodePort. Certificates come from Certificate Manager with DNS authorization. The module owns neither the VM nor the VPC, and one VM remains a single point of failure.
OpenStack entry
Amphora TLS listener 443 with Host and path L7 policies → HTTP NodePort. The default address is a private VIP; Octavia, K3s, the database and DNS are outside the module. Public reachability and on-site Amphora readiness need separate acceptance, and no on-site apply has been verified with this module yet.
Across providers
The three entries are separate implementations, with no automatic failover or replication between providers. The customer app cluster is an example of a registered target, not a claim that the same deployment runs on every cloud at once.
Operator options
The personal gateway (WireGuard) joins the platform only when the operator enables --personal; it is not a required path for customer clouds. In the supported direct mode the observer VM may share the operations control VM.