# SnapBench Scenario Generation Guide You are helping create scenarios for SnapBench, a platform for interactive Kubernetes-based labs. ## JSON Structure A scenario export has this structure: ```json { "scenario": { "name": "Scenario Name", "description": "Brief description of what participants will learn" }, "spec": { "ttlMinutes": 60, "displayMode": "canvas", "components": [...], "instructions": [...], "nodePositions": [...] }, "secrets": [] } ``` ## Components Components are the infrastructure pieces (databases, message brokers, apps) that run in the lab. ### Component Structure ```json { "id": "unique-lowercase-id", "kind": "DisplayName", "label": "template-label", "deploymentType": "docker|helm|compose", // Docker-specific "image": "image:tag", "command": ["/bin/bash", "-c"], "args": ["command to run"], "env": {"KEY": "value"}, // Helm-specific "helm": { "repository": "https://charts.example.com or oci://registry/path", "chart": "chart-name", "version": "1.0.0", "values": "yaml string with helm values" }, // Compose-specific (JSON object, not YAML string) "compose": { "services": { "service-name": { "image": "...", "environment": {...}, "ports": [...], "deploy": {"resources": {...}} } } }, // Common fields "resources": { "cpuM": 500, "memMi": 512 }, "tabs": [...], "dependsOn": ["other-component-id"], "readinessCheck": {...} } ``` ### Deployment Types #### Docker (simple containers) ```json { "id": "my-app", "kind": "MyApp", "label": "myapp", "deploymentType": "docker", "image": "nginx:latest", "expose": [ {"name": "http", "port": 80} ], "env": { "ENV_VAR": "value" }, "resources": { "cpuM": 100, "memMi": 128 }, "tabs": [ { "type": "terminal", "label": "Terminal", "workingDir": "/" } ] } ``` #### Helm (complex infrastructure) ```json { "id": "postgresql", "kind": "PostgreSQL", "label": "postgres", "deploymentType": "helm", "helm": { "repository": "oci://registry-1.docker.io/bitnamicharts", "chart": "postgresql", "version": "16.0.0", "values": "auth:\n username: postgres\n password: postgres\n database: app\nprimary:\n resources:\n requests:\n cpu: 250m\n memory: 512Mi" }, "resources": { "cpuM": 250, "memMi": 512 }, "tabs": [ { "type": "terminal", "label": "psql", "workingDir": "/tmp" } ] } ``` ### Common Helm Charts | Software | Repository | Chart | Typical Resources | |----------|------------|-------|-------------------| | PostgreSQL | `oci://registry-1.docker.io/bitnamicharts` | `postgresql` | 250m/512Mi | | MySQL | `oci://registry-1.docker.io/bitnamicharts` | `mysql` | 250m/512Mi | | Redis | `oci://registry-1.docker.io/bitnamicharts` | `redis` | 100m/256Mi | | Kafka | `oci://registry-1.docker.io/bitnamicharts` | `kafka` | 500m/1Gi | | Elasticsearch | `oci://registry-1.docker.io/bitnamicharts` | `elasticsearch` | 500m/1Gi | | MinIO | `oci://registry-1.docker.io/bitnamicharts` | `minio` | 250m/512Mi | | MongoDB | `oci://registry-1.docker.io/bitnamicharts` | `mongodb` | 250m/512Mi | ### Tabs (Participant Interface) Tabs define how participants interact with components. #### Terminal Tab ```json { "type": "terminal", "label": "Terminal", "workingDir": "/home" } ``` For Helm charts with multiple pods: ```json { "type": "terminal", "label": "Kafka CLI", "workingDir": "/opt/bitnami/kafka/bin", "podSelector": "controller", "containerSelector": "kafka" } ``` #### Web UI Tab To expose a web UI, you need THREE things for Docker components: 1. **`expose`** - Creates the Kubernetes Service 2. **`ui`** - Creates the Ingress for external browser access 3. **`tabs`** with `type: "web"` - Creates the clickable tab in SnapBench ```json { "id": "grafana", "deploymentType": "docker", "image": "grafana/grafana:11.0.0", "expose": [ {"name": "http", "port": 3000} ], "ui": [ {"path": "/", "servicePort": 3000} ], "tabs": [ { "type": "web", "label": "Dashboard", "port": 3000, "path": "/" } ] } ``` **UI structure:** ```json { "path": "/", // URL path prefix "servicePort": 3000 // Port on the service to route to } ``` For Helm charts: ```json { "type": "web", "label": "MinIO Console", "port": 9001, "path": "/", "serviceName": "minio" } ``` ### Readiness Check Ensures component is ready before dependent components start. **IMPORTANT: Be conservative with timing!** Some applications take a long time to start (e.g., Elasticsearch: 2-3 minutes). If your readiness check is too aggressive, the component will be marked as **failed** before it finishes starting. **Example Initial Delays** (indicative — always test and adjust): | Component | Example Initial Delay | |-----------|----------------------| | Redis, lightweight services | 5-10s | | PostgreSQL, MySQL | 10-15s | | Kafka | 15-30s | | Elasticsearch, Flink, heavy JVM apps | 60-120s | **TCP check** (for databases, message brokers): ```json { "readinessCheck": { "enabled": true, "type": "tcp", "tcpSocket": { "port": 5432 }, "initialDelaySeconds": 10, "periodSeconds": 5, "timeoutSeconds": 3 } } ``` **HTTP check** (for web services): ```json { "readinessCheck": { "enabled": true, "type": "http", "httpGet": { "port": 8080, "path": "/health" }, "initialDelaySeconds": 15, "periodSeconds": 5, "timeoutSeconds": 3 } } ``` **Slow-starting applications** (Elasticsearch, Flink, etc.): ```json { "readinessCheck": { "enabled": true, "type": "http", "httpGet": { "port": 9200, "path": "/_cluster/health" }, "initialDelaySeconds": 90, "periodSeconds": 10, "timeoutSeconds": 5 } } ``` ### Component Dependencies Use `dependsOn` to control startup order: ```json { "id": "app", "dependsOn": ["database", "cache"] } ``` ### Exposed Ports (CRITICAL for Docker components) **For Docker components, you MUST define `expose` to create a Kubernetes Service.** Without this, other components cannot reach this component by its label. ```json { "id": "kafka", "deploymentType": "docker", "image": "apache/kafka:3.7.0", "expose": [ {"name": "broker", "port": 9092} ] } ``` **Expose Port Structure:** ```json { "name": "port-name", // Required: descriptive name "port": 9092, // Required: port number "public": false, // Optional: expose publicly via gateway "protocol": "tcp", // Optional: "tcp" for TCP gateway, omit for HTTP "podSelector": "", // Optional: for Helm/Compose multi-pod "containerSelector": "" // Optional: for multi-container pods } ``` ### Public TCP Ports For components that need to be accessible from outside the cluster (e.g., Kafka brokers for external clients), set `public: true` and `protocol: "tcp"`: ```json { "expose": [ {"name": "internal", "port": 9092}, {"name": "external", "port": 9094, "public": true, "protocol": "tcp"} ] } ``` When a port is marked as public TCP: - A public endpoint is allocated (e.g., `tcp.preprod.snapbench.io:12345`) - Use `{{$self:public}}` to get the endpoint (returns TCP `host:port`) - Use `{{$self:public:9094}}` to get a specific port's endpoint **Example: Kafka with External Access** ```json { "id": "kafka", "kind": "Kafka", "label": "kafka", "deploymentType": "docker", "image": "apache/kafka:3.7.0", "env": { "KAFKA_LISTENERS": "INTERNAL://:9092,EXTERNAL://:9094,CONTROLLER://:9093", "KAFKA_ADVERTISED_LISTENERS": "INTERNAL://{{$self}}:9092,EXTERNAL://{{$self:public:9094}}", "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP": "INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT", "KAFKA_INTER_BROKER_LISTENER_NAME": "INTERNAL", "KAFKA_CONTROLLER_LISTENER_NAMES": "CONTROLLER", "KAFKA_CONTROLLER_QUORUM_VOTERS": "1@localhost:9093", "KAFKA_NODE_ID": "1", "KAFKA_PROCESS_ROLES": "broker,controller", "CLUSTER_ID": "MkU3OEVBNTcwNTJENDM2Qk" }, "expose": [ {"name": "internal", "port": 9092}, {"name": "external", "port": 9094, "public": true, "protocol": "tcp"} ], "resources": {"cpuM": 500, "memMi": 1024} } ``` In this example, `{{$self:public:9094}}` resolves to the TCP endpoint (e.g., `tcp.preprod.snapbench.io:12345`), allowing external Kafka clients to connect. **Behavior by deployment type:** | Type | `expose` empty | Service created? | |------|----------------|------------------| | Docker | Yes | NO - Component unreachable by other components | | Docker | No (ports defined) | YES - ClusterIP service | | Compose | Any | YES - Always created (headless if no ports) | | Helm | N/A | YES - Managed by chart | **Common ports to expose:** | Service | Port | |---------|------| | PostgreSQL | 5432 | | MySQL | 3306 | | Redis | 6379 | | Kafka | 9092 | | Elasticsearch | 9200 | | MongoDB | 27017 | | HTTP services | 8080, 3000, etc. | ### Inter-Component References Components can reference each other using the `label` field: - In Helm values or env vars, use the label as hostname - Example: if PostgreSQL has `"label": "postgres"`, other components connect to `postgres:5432` - **Important**: The target component MUST have `expose` defined with that port for Docker deployments ### Secret References Inject secret values into environment variables, Helm values, or files using the `{{secret:name:key}}` syntax: ```json { "env": { "LICENSE_KEY": "{{secret:my-license:key}}", "DB_PASSWORD": "{{secret:db-creds:password}}" } } ``` **In Helm values:** ```yaml auth: password: "{{secret:postgres-auth:password}}" vvp: license: "{{secret:vvp-license:key}}" ``` **Secret structure in export:** ```json { "secrets": [ { "name": "my-license", "type": "opaque", "keyValues": { "key": "LICENSE-KEY-VALUE-HERE" } }, { "name": "db-creds", "type": "opaque", "keyValues": { "username": "admin", "password": "secret123" } } ] } ``` **Supported secret types:** - `opaque`: Generic key-value pairs (most common for template references) - `docker-registry`: Container registry credentials - `tls`: TLS certificate and key - `basic-auth`: Username and password ## Instructions Instructions guide participants through the lab. They support Markdown with special features. ### Instruction Structure ```json { "title": "Step Title (shown in sidebar)", "markdown": "# Full Markdown Content\n\nWith **formatting** and `code`." } ``` ### Tab Navigation Links Create clickable buttons that switch to component tabs: ```markdown Open the [Kafka Terminal](tab:kafka) to run commands. ``` Format: `[link text](tab:componentLabel)` or `[link text](tab:componentLabel:tabIndex)` - `tab:kafka` → opens first tab of component with label "kafka" - `tab:kafka:0` → opens first tab (index 0) - `tab:kafka:1` → opens second tab (index 1) ### Code Blocks Terminal commands (dark theme): ````markdown ```bash kafka-topics.sh --create --topic events \ --bootstrap-server localhost:9092 ``` ```` Code snippets (light theme): ````markdown ```sql SELECT * FROM users WHERE active = true; ``` ```` ````markdown ```json {"key": "value"} ``` ```` ### Best Practices for Instructions 1. **Start with context** - Explain what the participant will do and why 2. **One task per instruction** - Keep steps focused 3. **Show expected output** - Tell participants what success looks like 4. **Use tab links** - Guide participants to the right terminal/UI 5. **Include troubleshooting tips** - Use blockquotes for common issues Example instruction: ```markdown # Create a Kafka Topic In this step, you'll create a topic to store user events. Open the [Kafka Terminal](tab:kafka) and run: ```bash kafka-topics.sh --create \ --topic user-events \ --bootstrap-server localhost:9092 \ --partitions 3 ``` You should see: ``` Created topic user-events. ``` > **Tip**: If you see a connection error, wait 30 seconds for Kafka to fully initialize. ``` ## Node Positions (Canvas Layout) Position components visually on the canvas: ```json { "nodePositions": [ {"id": "database", "x": 100, "y": 200}, {"id": "app", "x": 400, "y": 200}, {"id": "cache", "x": 400, "y": 400} ] } ``` - x: horizontal position (0 = left) - y: vertical position (0 = top) - Typical spacing: 200-300px between components ## Complete Example ```json { "scenario": { "name": "PostgreSQL Basics", "description": "Learn SQL fundamentals with a real PostgreSQL database" }, "spec": { "ttlMinutes": 60, "displayMode": "canvas", "components": [ { "id": "postgres", "kind": "PostgreSQL", "label": "postgres", "deploymentType": "docker", "image": "postgres:16-alpine", "env": { "POSTGRES_USER": "postgres", "POSTGRES_PASSWORD": "postgres", "POSTGRES_DB": "demo" }, "expose": [ {"name": "postgres", "port": 5432} ], "resources": { "cpuM": 250, "memMi": 512 }, "tabs": [ { "type": "terminal", "label": "psql", "workingDir": "/tmp" } ], "readinessCheck": { "enabled": true, "type": "tcp", "tcpSocket": {"port": 5432}, "initialDelaySeconds": 10, "periodSeconds": 5, "timeoutSeconds": 3 } } ], "instructions": [ { "title": "Welcome", "markdown": "# PostgreSQL Basics\n\nIn this lab, you'll learn SQL fundamentals using a real PostgreSQL database.\n\n## What you'll learn\n\n- Creating tables\n- Inserting data\n- Querying with SELECT\n- Filtering with WHERE\n\nLet's get started!" }, { "title": "Connect to PostgreSQL", "markdown": "# Connect to the Database\n\nOpen the [psql terminal](tab:postgres) and connect:\n\n```bash\npsql -U postgres -d demo\n```\n\nYou should see the PostgreSQL prompt:\n```\ndemo=#\n```" }, { "title": "Create a Table", "markdown": "# Create Your First Table\n\nIn the [psql terminal](tab:postgres), run:\n\n```sql\nCREATE TABLE users (\n id SERIAL PRIMARY KEY,\n name VARCHAR(100) NOT NULL,\n email VARCHAR(255) UNIQUE,\n created_at TIMESTAMP DEFAULT NOW()\n);\n```\n\nVerify with:\n```sql\n\\dt\n```" }, { "title": "Insert Data", "markdown": "# Insert Some Data\n\nAdd users to your table:\n\n```sql\nINSERT INTO users (name, email) VALUES\n ('Alice', 'alice@example.com'),\n ('Bob', 'bob@example.com'),\n ('Charlie', 'charlie@example.com');\n```\n\nCheck the result:\n```sql\nSELECT * FROM users;\n```" }, { "title": "Query Data", "markdown": "# Query Your Data\n\nTry these queries:\n\n```sql\n-- Find a specific user\nSELECT * FROM users WHERE name = 'Alice';\n\n-- Count users\nSELECT COUNT(*) FROM users;\n\n-- Order by creation date\nSELECT name, created_at FROM users ORDER BY created_at DESC;\n```\n\n## Congratulations!\n\nYou've learned the basics of SQL with PostgreSQL." } ], "nodePositions": [ {"id": "postgres", "x": 250, "y": 200} ] }, "secrets": [] } ``` ## Guidelines for Scenario Generation 1. **Match complexity to learning goal** - Don't add components that aren't needed 2. **Estimate resources** - Total should stay under 2000m CPU and 4Gi memory 3. **Set appropriate TTL** - 30-60 min for simple labs, 90-120 min for complex ones 4. **Include readiness checks** - Especially for databases and services that take time to start 5. **Use dependsOn** - Ensure proper startup order 6. **Write clear instructions** - Action-oriented, with expected outcomes 7. **Position nodes logically** - Data flow left-to-right or top-to-bottom ## Best Practices for Docker Components ### Interactive Containers (Workbenches) For containers where participants need to run commands interactively, use this pattern: ```json { "id": "workbench", "kind": "Workbench", "label": "workbench", "deploymentType": "docker", "image": "python:3.11-slim", "args": ["sleep", "infinity"], "tabs": [ { "type": "terminal", "label": "Terminal", "workingDir": "/sb" } ], "files": [ {"name": "script.py", "content": "..."} ] } ``` **Key points:** - Use `"args": ["sleep", "infinity"]` to keep the container running - Do NOT set `command` - leave it empty/undefined - This allows participants to run any command interactively in the terminal - Use `files` to provide scripts that participants can execute manually - Files are mounted at `/sb/{filename}` **Anti-pattern - DON'T do this:** ```json { "command": ["/bin/bash", "-c"], "args": ["pip install foo && python script.py && tail -f /dev/null"] } ``` This runs a fixed script and doesn't allow interactive use. ### Services That Run Automatically For services that should start and run without user interaction (databases, brokers, etc.): ```json { "id": "kafka", "deploymentType": "docker", "image": "apache/kafka:3.7.0", "env": { "KAFKA_NODE_ID": "1", ... } } ``` Don't override `command` or `args` unless necessary - let the image's default entrypoint run. ### Installing Dependencies If a workbench needs dependencies installed before use, add installation to the first instruction step rather than the container startup: ```markdown # Setup First, install the required packages: ```bash pip install kafka-python psycopg2-binary ``` Now you're ready to run the scripts. ``` This keeps the container startup simple and lets participants see what's being installed. ## Common Component Patterns ### Kafka (KRaft mode, no Zookeeper) ```json { "id": "kafka", "kind": "Kafka", "label": "kafka", "deploymentType": "docker", "image": "apache/kafka:3.7.0", "env": { "KAFKA_NODE_ID": "1", "KAFKA_PROCESS_ROLES": "broker,controller", "KAFKA_LISTENERS": "PLAINTEXT://:9092,CONTROLLER://:9093", "KAFKA_ADVERTISED_LISTENERS": "PLAINTEXT://kafka:9092", "KAFKA_CONTROLLER_LISTENER_NAMES": "CONTROLLER", "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP": "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT", "KAFKA_CONTROLLER_QUORUM_VOTERS": "1@localhost:9093", "KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR": "1", "KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR": "1", "KAFKA_TRANSACTION_STATE_LOG_MIN_ISR": "1", "KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS": "0", "CLUSTER_ID": "MkU3OEVBNTcwNTJENDM2Qk" }, "expose": [{"name": "broker", "port": 9092}], "resources": {"cpuM": 500, "memMi": 1024}, "tabs": [ {"type": "terminal", "label": "Kafka CLI", "workingDir": "/opt/kafka/bin"} ], "readinessCheck": { "enabled": true, "type": "tcp", "tcpSocket": {"port": 9092}, "initialDelaySeconds": 15, "periodSeconds": 5, "timeoutSeconds": 3 } } ``` ### PostgreSQL (Docker) ```json { "id": "postgres", "kind": "PostgreSQL", "label": "postgres", "deploymentType": "docker", "image": "postgres:16-alpine", "env": { "POSTGRES_USER": "postgres", "POSTGRES_PASSWORD": "postgres", "POSTGRES_DB": "demo" }, "expose": [{"name": "postgres", "port": 5432}], "resources": {"cpuM": 250, "memMi": 512}, "tabs": [ {"type": "terminal", "label": "psql", "workingDir": "/"} ], "readinessCheck": { "enabled": true, "type": "tcp", "tcpSocket": {"port": 5432}, "initialDelaySeconds": 5, "periodSeconds": 3, "timeoutSeconds": 2 } } ``` ### Python Workbench ```json { "id": "workbench", "kind": "Workbench", "label": "workbench", "deploymentType": "docker", "image": "python:3.11-slim", "args": ["sleep", "infinity"], "resources": {"cpuM": 200, "memMi": 256}, "tabs": [ {"type": "terminal", "label": "Python", "workingDir": "/sb"} ], "files": [ {"name": "example.py", "content": "print('Hello from SnapBench!')"} ] } ``` ### Grafana (with Web UI) ```json { "id": "grafana", "kind": "Grafana", "label": "grafana", "deploymentType": "docker", "image": "grafana/grafana:11.0.0", "env": { "GF_SECURITY_ADMIN_USER": "admin", "GF_SECURITY_ADMIN_PASSWORD": "admin", "GF_AUTH_ANONYMOUS_ENABLED": "true" }, "expose": [{"name": "http", "port": 3000}], "ui": [{"path": "/", "servicePort": 3000}], "resources": {"cpuM": 200, "memMi": 256}, "tabs": [ {"type": "web", "label": "Grafana", "port": 3000, "path": "/"} ], "readinessCheck": { "enabled": true, "type": "http", "httpGet": {"port": 3000, "path": "/api/health"}, "initialDelaySeconds": 10, "periodSeconds": 5, "timeoutSeconds": 3 } } ``` ### MinIO (S3-Compatible Storage) ```json { "id": "minio", "kind": "MinIO", "label": "minio", "deploymentType": "docker", "image": "minio/minio:latest", "env": { "MINIO_ROOT_USER": "minioadmin", "MINIO_ROOT_PASSWORD": "minioadmin" }, "args": ["server", "/data", "--console-address", ":9001"], "expose": [ {"name": "api", "port": 9000}, {"name": "console", "port": 9001} ], "ui": [{"path": "/", "servicePort": 9001}], "resources": {"cpuM": 250, "memMi": 512}, "tabs": [ {"type": "web", "label": "MinIO Console", "port": 9001, "path": "/"}, {"type": "terminal", "label": "Terminal", "workingDir": "/data"} ], "readinessCheck": { "enabled": true, "type": "http", "httpGet": {"port": 9000, "path": "/minio/health/live"}, "initialDelaySeconds": 5, "periodSeconds": 5, "timeoutSeconds": 3 } } ```