Skip to content

Installation Linux Pulumi

boyism80 edited this page Jul 4, 2026 · 1 revision

Linux — Pulumi

Optional infrastructure-as-code layer for Kubernetes. Use Pulumi when you want programmatic deploy, CI parity, and multi-world management from develop.json — you do not need Pulumi to run fb on Kubernetes if you maintain your own manifests.

Parent guide: Linux Installation

For single-host Docker without Kubernetes, see Docker Compose.


Overview

Layer How it runs
Build Docker images (server/*/Dockerfile) pushed to GHCR
Deploy Pulumi (infra/pulumi/) creates resources in namespace fb
Config infra/pulumi/develop.json + Pulumi secret host
Data fb/data image (Excel → JSON); maps bundled in game image

Typical workflows:

  1. CI (recommended) — push to release/** triggers image build and Pulumi deploy (.github/workflows/build.yml).
  2. Manual full cycle — run tools/build.sh on a build machine (build, push, set host, pulumi up).
  3. Deploy only — images already in registry; run Pulumi with the correct host value.

When to use Pulumi

Use Pulumi Use plain Kubernetes instead
CI deploy on release/** (repo default) Custom cluster, hand-written YAML
Multi-world from develop.json Single world (dev), minimal shards in infra/k8s/
Team already uses Pulumi Prefer kubectl / GitOps only

The checked-in dev overlay (infra/k8s/overlays/dev) uses the same StatefulSet/Service naming as Pulumi — only replica and shard counts are reduced.

Images are always built with Docker first — see Kubernetes — Build images.


Prerequisites

Kubernetes cluster

Pulumi targets the cluster from your current kubeconfig (kubectl config current-context).

Stock manifests assume a self-hosted cluster with:

Requirement Used by Notes
Node label infra=true MySQL, Redis, RabbitMQ StatefulSets Required
Node label cpu=epyc Gateway, Login, Game, Internal, … Preferred only
Host path /mnt/fb/ MySQL, Redis data volumes DirectoryOrCreate on infra nodes
NodePort range Client-facing services Fixed ports in develop.json (e.g. gateway 30000)

If your cluster uses different labels or storage, update affinity / hostPath in infra/pulumi/*.js before deploying.

Build machine (image build)

Tool Purpose
Docker + buildx Multi-stage builds (tools/build.sh, CI)
Git Clone with submodules
Registry login Push to ghcr.io (or retag to your registry)

tools/build.sh uses sudo docker when the current user cannot access the Docker daemon.

Deploy machine (Pulumi)

Tool Purpose
Node.js (LTS) npm install in infra/pulumi/
Pulumi CLI pulumi up / pulumi destroy
kubectl Cluster access
Pulumi access token If using Pulumi Cloud (pulumi login)

CI uses stack name dev (.github/workflows/build.yml deploy job).


What you must customize

1. Pulumi secret host (required)

host is the IP or hostname clients use to reach Gateway, Login, and Game. It is injected at deploy time, not stored in develop.json:

conf.host = new pulumi.Config().require('host')

Set before every deploy when the public address may change:

cd infra/pulumi
pulumi stack select dev
pulumi config set --secret host "<YOUR_EXTERNAL_IP_OR_HOSTNAME>"

tools/build.sh sets this from the machine's public IP (curl -s https://ifconfig.me).

Situation What to do
Clients via public IP Use that IP, or build.sh / ifconfig.me
LAN only Node LAN IP (not 127.0.0.1)
DNS name FQDN clients resolve
NAT / wrong auto-detect Set host manually

CI passes the secret as REMOTE_IP in the deploy job.

2. infra/pulumi/develop.json

Main static config: worlds, ports, replicas, client metadata.

Section What to change
worlds.<name>.deploy true to deploy, false to skip
worlds.<name>.login.port Login NodePort
worlds.<name>.game.containers Game shards and NodePorts
worlds.<name>.mysql / redis Per-world DB/cache ports
gateway.port Gateway NodePort (default 30000)
internal.port, marketplace.port, admin-tool.port HTTP NodePorts
unified-infra Shared MySQL, Redis, RabbitMQ
database.autoMigration EF migrations on startup
admin-tool.security.elevationSecret Change for non-dev

Default: only world-1 and world-2 have "deploy": true. Others are templates with "deploy": false.

Port collisions: node values must be unique within the NodePort range (typically 30000–32767).

3. Container image registry

Pulumi references ghcr.io/boyism80/fb/<service>:latest. tools/build.sh pushes to the same paths.

For your own registry: retag images and update strings in infra/pulumi/*.js.

4. Node labels and storage

Without infra=true on at least one node, MySQL/Redis/RabbitMQ pods stay Pending.

Data paths on infra nodes:

/mnt/fb/mysql/<world>/...
/mnt/fb/redis/<world>/...

5. Credentials (optional hardening)

MySQL and RabbitMQ defaults are in Pulumi (fb / admin). Change in mysql.js, rabbitmq.js, and connection strings in service modules for production.

6. Pulumi stack and backend

  • CI stack: dev
  • Local: pulumi stack init dev or pulumi stack select dev

Path A — CI deploy (release branch)

On push to release/**, .github/workflows/build.yml:

  1. build-linux — builds and pushes images to ghcr.io/<owner>/fb/*
  2. Per-service jobs — parallel image builds
  3. deploypulumi destroy then pulumi up on stack dev with host from REMOTE_IP

Required GitHub secrets:

Secret Purpose
PULUMI_ACCESS_TOKEN Pulumi Cloud API
REMOTE_IP Client-facing host value

Self-hosted runners need Docker, kubeconfig, and GHCR access.


Path B — Manual build and deploy (tools/build.sh)

From the repository root on a machine that can push images and run Pulumi:

bash tools/build.sh Release

What the script does:

  1. tools/prune-build-cache.sh — prune old BuildKit cache
  2. Build and push images: databuild → gateway, login, game, bot, internal, write-back, admin-tool, log, marketplace
  3. pulumi config set --secret host "$EXTERNAL_IP"
  4. pulumi down -y && pulumi up -ydestroys then recreates the stack

Warning: pulumi down deletes all stack-managed resources. For incremental updates, use pulumi up without down.

Override external IP:

cd infra/pulumi
npm install
pulumi config set --secret host "203.0.113.10"
pulumi up -y

Path C — Deploy only (no image rebuild)

When images are already in the registry:

cd infra/pulumi
npm install
pulumi stack select dev
pulumi config set --secret host "<YOUR_EXTERNAL_IP_OR_HOSTNAME>"
pulumi up -y

Use pulumi refresh if state drifted. Use pulumi destroy to tear down namespace fb resources.


Client connection ports (defaults)

With stock develop.json and world-1 + world-2 deployed:

Service NodePort Notes
Gateway 30000 Client entry
Login (world-1) 30010 World "연"
Login (world-2) 30011 World "무휼"
Game (world-1) 3003030038 Per container in game.containers
Internal (HTTP) 30200 Ops / API
Admin tool 30210
Marketplace 30220

Game and Login addresses sent to clients use Pulumi host, not in-cluster DNS.


Boot order (Pulumi)

infra/pulumi/index.js orchestrates:

  1. Namespace fb
  2. MySQL, Redis, RabbitMQ (parallel)
  3. Internal, Write-back, Admin Tool, Log, Marketplace
  4. Gateway, Login, Game
  5. Bot (commented out in index.js)

Worlds with "deploy": false are stripped before resources are created.


Verify deployment

kubectl -n fb get pods
kubectl -n fb get svc
pulumi stack output
nc -zv <host> 30000
pulumi config get host

With database.autoMigration: true, Internal applies migrations from infra/db/migrations/ on first start.


Troubleshooting

Symptom Likely cause
MySQL/Redis pods Pending No node with infra=true
Client cannot connect Wrong host; pulumi config set --secret host + pulumi up
ImagePullBackOff Registry auth or wrong image name
NodePort already allocated Duplicate port in develop.json
Data loss concern after deploy pulumi down removes K8s resources; hostPath data under /mnt/fb/ remains unless deleted on disk
ifconfig.me wrong IP Set host manually

Related files

Path Role
tools/build.sh Build all images, set host, Pulumi up
tools/prune-build-cache.sh BuildKit cache cleanup
infra/pulumi/develop.json Worlds, ports, replicas
infra/pulumi/index.js Deploy graph and host injection
infra/pulumi/*.js Per-service Kubernetes manifests
.github/workflows/build.yml CI build + deploy
server/*/Dockerfile Service image definitions

Related pages

Clone this wiki locally