|
| 1 | +# interactive-fleets |
| 2 | + |
| 3 | +A general-purpose **submit-and-poll** pattern for IBM Code Engine Fleets. |
| 4 | +A Code Engine **Function** acts as a synchronous HTTP endpoint. It accepts any JSON data, |
| 5 | +dispatches it as a fleet task, polls COS for the result, and returns the enriched output — |
| 6 | +or a retrieval URL if the task is still running when the poll window closes. |
| 7 | + |
| 8 | +This sample ships a simple **data enrichment** worker (adds timestamp, hostname, |
| 9 | +word/character counts, and reversed key list) as the fleet task. Swap out the |
| 10 | +enrichment logic in [`task-container/entrypoint.sh`](task-container/entrypoint.sh) |
| 11 | +with any workload: ML inference, document processing, data transformation, etc. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Architecture |
| 16 | + |
| 17 | +```mermaid |
| 18 | +flowchart TD |
| 19 | + CallerPost["Caller — POST /\nJSON body with arbitrary data"] |
| 20 | + CallerGet["Caller — GET /\n?key=value&... query params"] |
| 21 | + CallerResult["Caller — GET /result/{task_id}\nRetrieve previously submitted result"] |
| 22 | +
|
| 23 | + Fn["Code Engine Function · function/__main__.py\n─────────────────────────────────────\nGenerates task_id (UUID)\nSerialises input dict as JSON string\nObtains IAM token via CE_API_KEY → submits task\n add_tasks: {idx, args: ['<json-string>']}\nPolls COS for {COS_PREFIX}/{task_id}.json\n via IAM Bearer token up to POLL_TIMEOUT s\nHTTP 200 — result returned synchronously\nHTTP 202 — timeout; returns task_id + retrieve_url\nGET /result/{task_id} → direct COS lookup (IAM)"] |
| 24 | +
|
| 25 | + subgraph Fleet["Code Engine Fleet · cx3-4x8 (4 vCPU · 8 GiB)"] |
| 26 | + Task["Task Container · task-container/entrypoint.sh\n─────────────────────────────────────\n1 vCPU / 2 GiB\nReceives input as JSON string in $1\nEnriches payload:\n + timestamp, processing_node (hostname)\n + word_count, char_count\n + reversed_keys, summary\nWrites result to /data/results/interactive-fleet-result/{task_id}.json"] |
| 27 | + end |
| 28 | +
|
| 29 | + COS["COS Persistent Data Store · fleet-output-store\ninteractive-fleet-result/{task_id}.json\n{task_id, status, elapsed_seconds,\n enriched_at, processing_node,\n input: {...}, output: {...}}"] |
| 30 | +
|
| 31 | + CallerPost -->|"HTTP POST — submit + poll"| Fn |
| 32 | + CallerGet -->|"HTTP GET — submit + poll"| Fn |
| 33 | + CallerResult-->|"HTTP GET — retrieve only"| Fn |
| 34 | + Fn -->|"CE v2 REST API POST .../add_tasks"| Fleet |
| 35 | + Fn -.->|"S3 REST API GET /{bucket}/interactive-fleet-result/{task_id}.json\n(IAM Bearer — polls up to POLL_TIMEOUT seconds)"| COS |
| 36 | + Task -->|"COS mount /data/results/interactive-fleet-result/{task_id}.json"| COS |
| 37 | +``` |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Files |
| 42 | + |
| 43 | +| File | Purpose | |
| 44 | +|------|---------| |
| 45 | +| [`deploy`](deploy) | Single deploy script — builds image, creates fleet, creates secret, deploys function | |
| 46 | +| [`task-container/Dockerfile`](task-container/Dockerfile) | Fleet task image — ubi-minimal + bash + jq | |
| 47 | +| [`task-container/entrypoint.sh`](task-container/entrypoint.sh) | Enrichment worker — receives argv key-value pairs, writes `/data/results/interactive-fleet-result/{task_id}.json` | |
| 48 | +| [`function/__main__.py`](function/__main__.py) | CE Function — routes requests, submits tasks, polls COS, returns results | |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +## Prerequisites |
| 53 | + |
| 54 | +- IBM Cloud CLI with Code Engine plugin |
| 55 | + ```bash |
| 56 | + ibmcloud plugin install code-engine |
| 57 | + ``` |
| 58 | +- A Code Engine project selected |
| 59 | + ```bash |
| 60 | + ibmcloud ce project select --name <project> |
| 61 | + ``` |
| 62 | +- Persistent data stores provisioned (e.g. via `init-fleet-sandbox`): |
| 63 | + ```bash |
| 64 | + ibmcloud ce pds create --name fleet-task-store ... |
| 65 | + ibmcloud ce pds create --name fleet-output-store ... |
| 66 | + ``` |
| 67 | +- Subnetpool `fleet-subnetpool` provisioned |
| 68 | +- Container registry secret `ce-auto-icr-private-eu-de` (or set `FLEET_REGISTRY_SECRET`) |
| 69 | +- Your IBM Container Registry namespace: |
| 70 | + ```bash |
| 71 | + export ICR_NAMESPACE=<your-icr-namespace> |
| 72 | + ``` |
| 73 | +- An IBM Cloud API key with **Code Engine Writer** and **COS Reader** access: |
| 74 | + ```bash |
| 75 | + export CE_API_KEY=<your-api-key> |
| 76 | + ``` |
| 77 | +
|
| 78 | +--- |
| 79 | +
|
| 80 | +## Deployment |
| 81 | +
|
| 82 | +```bash |
| 83 | +export ICR_NAMESPACE=<your-namespace> |
| 84 | +export CE_API_KEY=<your-api-key> |
| 85 | + |
| 86 | +./deploy |
| 87 | +``` |
| 88 | +
|
| 89 | +The script prints the function endpoint at the end. Copy it for the examples below. |
| 90 | +
|
| 91 | +```bash |
| 92 | +export FN_URL=https://<function-name>.<subdomain>.<region>.codeengine.appdomain.cloud |
| 93 | +``` |
| 94 | +
|
| 95 | +### Deploy options |
| 96 | +
|
| 97 | +| Variable | Default | Description | |
| 98 | +|---|---|---| |
| 99 | +| `SKIP_BUILD=1` | — | Skip image build (reuse existing image) | |
| 100 | +| `SKIP_FLEET=1` | — | Skip fleet creation; `FLEET_ID` must be set | |
| 101 | +| `SKIP_SECRET=1` | — | Skip secret creation/update | |
| 102 | +| `FLEET_MAX_SCALE` | `2` | Maximum fleet worker nodes | |
| 103 | +| `POLL_TIMEOUT` | `30` | Seconds the function polls COS before returning 202 | |
| 104 | +
|
| 105 | +--- |
| 106 | +
|
| 107 | +## Usage |
| 108 | +
|
| 109 | +### 1. Submit a task and wait for the result (synchronous) |
| 110 | +
|
| 111 | +The function submits the task to the fleet, polls COS for up to `POLL_TIMEOUT` seconds, |
| 112 | +and returns the enriched result directly when it is ready. |
| 113 | +
|
| 114 | +**POST with a JSON body:** |
| 115 | +
|
| 116 | +```bash |
| 117 | +curl -s -X POST "${FN_URL}" \ |
| 118 | + -H "Content-Type: application/json" \ |
| 119 | + -d '{ |
| 120 | + "name": "Alice", |
| 121 | + "city": "Berlin", |
| 122 | + "message": "Hello from interactive-fleets" |
| 123 | + }' | jq . |
| 124 | +``` |
| 125 | +
|
| 126 | +**GET with query parameters:** |
| 127 | +
|
| 128 | +```bash |
| 129 | +curl -s "${FN_URL}?name=Alice&city=Berlin&message=Hello+from+interactive-fleets" | jq . |
| 130 | +``` |
| 131 | +
|
| 132 | +**HTTP 200 — result available within poll window:** |
| 133 | +
|
| 134 | +```json |
| 135 | +{ |
| 136 | + "task_id": "a3f5b8c2-4d1e-4b5a-9c3f-1e2d3c4b5a6f", |
| 137 | + "status": "completed", |
| 138 | + "result": { |
| 139 | + "task_id": "a3f5b8c2-4d1e-4b5a-9c3f-1e2d3c4b5a6f", |
| 140 | + "status": "completed", |
| 141 | + "elapsed_seconds": 0.0021, |
| 142 | + "enriched_at": "2025-01-15T12:00:00.123456Z", |
| 143 | + "processing_node": "worker-abc123", |
| 144 | + "input": { |
| 145 | + "name": "Alice", |
| 146 | + "city": "Berlin", |
| 147 | + "message": "Hello from interactive-fleets" |
| 148 | + }, |
| 149 | + "output": { |
| 150 | + "word_count": 5, |
| 151 | + "char_count": 29, |
| 152 | + "reversed_keys": ["message", "city", "name"], |
| 153 | + "summary": "Enriched 3 field(s) from input." |
| 154 | + } |
| 155 | + } |
| 156 | +} |
| 157 | +``` |
| 158 | +
|
| 159 | +--- |
| 160 | +
|
| 161 | +### 2. Submit a task asynchronously (fire and forget) |
| 162 | +
|
| 163 | +Set `poll_timeout=0` to skip polling entirely and get the retrieval URL immediately. |
| 164 | +
|
| 165 | +```bash |
| 166 | +curl -s -X POST "${FN_URL}" \ |
| 167 | + -H "Content-Type: application/json" \ |
| 168 | + -d '{ |
| 169 | + "name": "Bob", |
| 170 | + "city": "London", |
| 171 | + "poll_timeout": 0 |
| 172 | + }' | jq . |
| 173 | +``` |
| 174 | +
|
| 175 | +**HTTP 202 — task accepted, result not yet available:** |
| 176 | +
|
| 177 | +```json |
| 178 | +{ |
| 179 | + "message": "Task submitted and is being processed", |
| 180 | + "task_id": "b4e6c9d2-5e2f-4c6b-8d4e-2f3e4d5c6b7e", |
| 181 | + "status": "pending", |
| 182 | + "fleet_id": "1a2b3c4d-...", |
| 183 | + "poll_timeout_seconds": 0, |
| 184 | + "retrieve_url": "https://<fn-url>/result/b4e6c9d2-5e2f-4c6b-8d4e-2f3e4d5c6b7e" |
| 185 | +} |
| 186 | +``` |
| 187 | +
|
| 188 | +--- |
| 189 | +
|
| 190 | +### 3. Retrieve a result by task ID |
| 191 | +
|
| 192 | +Use the `retrieve_url` from the 202 response, or construct it manually: |
| 193 | +
|
| 194 | +```bash |
| 195 | +curl -s "${FN_URL}/result/b4e6c9d2-5e2f-4c6b-8d4e-2f3e4d5c6b7e" | jq . |
| 196 | +``` |
| 197 | +
|
| 198 | +**HTTP 200** — result is ready: |
| 199 | +
|
| 200 | +```json |
| 201 | +{ |
| 202 | + "task_id": "b4e6c9d2-...", |
| 203 | + "status": "completed", |
| 204 | + "elapsed_seconds": 0.0018, |
| 205 | + "enriched_at": "2025-01-15T12:01:15.000000Z", |
| 206 | + "processing_node": "worker-def456", |
| 207 | + "input": { "name": "Bob", "city": "London" }, |
| 208 | + "output": { |
| 209 | + "word_count": 2, |
| 210 | + "char_count": 10, |
| 211 | + "reversed_keys": ["city", "name"], |
| 212 | + "summary": "Enriched 2 field(s) from input." |
| 213 | + } |
| 214 | +} |
| 215 | +``` |
| 216 | +
|
| 217 | +**HTTP 404** — task is still running or ID is invalid: |
| 218 | +
|
| 219 | +```json |
| 220 | +{ |
| 221 | + "task_id": "b4e6c9d2-...", |
| 222 | + "status": "pending_or_not_found", |
| 223 | + "retrieve_url": "https://<fn-url>/result/b4e6c9d2-..." |
| 224 | +} |
| 225 | +``` |
| 226 | +
|
| 227 | +--- |
| 228 | +
|
| 229 | +### 4. Provide a custom task ID |
| 230 | +
|
| 231 | +```bash |
| 232 | +curl -s -X POST "${FN_URL}" \ |
| 233 | + -H "Content-Type: application/json" \ |
| 234 | + -d '{ |
| 235 | + "task_id": "my-custom-task-001", |
| 236 | + "product": "widget", |
| 237 | + "quantity": "42" |
| 238 | + }' | jq . |
| 239 | +``` |
| 240 | +
|
| 241 | +The result will be stored at `interactive-fleet-result/my-custom-task-001.json` in the COS bucket. |
| 242 | +
|
| 243 | +--- |
| 244 | +
|
| 245 | +## Result JSON schema |
| 246 | +
|
| 247 | +The fleet task writes the following structure to `interactive-fleet-result/{task_id}.json`: |
| 248 | +
|
| 249 | +```json |
| 250 | +{ |
| 251 | + "task_id": "<uuid or custom id>", |
| 252 | + "status": "completed", |
| 253 | + "elapsed_seconds": 0.0021, |
| 254 | + "enriched_at": "ISO-8601 UTC timestamp", |
| 255 | + "processing_node": "hostname of the fleet worker", |
| 256 | + "input": { |
| 257 | + "...": "all key/value pairs from the original request (control keys stripped)" |
| 258 | + }, |
| 259 | + "output": { |
| 260 | + "word_count": 42, |
| 261 | + "char_count": 210, |
| 262 | + "reversed_keys": ["key3", "key2", "key1"], |
| 263 | + "summary": "Enriched N field(s) from input." |
| 264 | + } |
| 265 | +} |
| 266 | +``` |
| 267 | +
|
| 268 | +--- |
| 269 | +
|
| 270 | +## Configuration reference |
| 271 | +
|
| 272 | +### Function environment variables |
| 273 | +
|
| 274 | +| Variable | Default | Description | |
| 275 | +|---|---|---| |
| 276 | +| `FLEET_ID` | **required** | Target fleet UUID or name | |
| 277 | +| `CE_API_KEY` | **required** (from `interactive-fleet-secrets`) | IBM Cloud API key — IAM token used for CE Fleet API **and** COS access | |
| 278 | +| `COS_BUCKET` | `fleet-output-store` | COS bucket where results are stored | |
| 279 | +| `COS_PREFIX` | `interactive-fleet-result` | Folder prefix inside the COS bucket | |
| 280 | +| `COS_ENDPOINT` | `https://s3.direct.eu-de.cloud-object-storage.appdomain.cloud` | COS S3 endpoint | |
| 281 | +| `POLL_TIMEOUT` | `30` | Max seconds to poll COS before returning HTTP 202 | |
| 282 | +| `POLL_INTERVAL` | `2` | Seconds between COS poll attempts | |
| 283 | +
|
| 284 | +### Task container environment variables |
| 285 | +
|
| 286 | +| Variable | Default | Description | |
| 287 | +|---|---|---| |
| 288 | +| `RESULTS_DIR` | `/data/results` | Fleet COS mount root; script writes `interactive-fleet-result/<task_id>.json` inside it | |
| 289 | +
|
| 290 | +### Deploy script environment variables |
| 291 | +
|
| 292 | +| Variable | Default | Description | |
| 293 | +|---|---|---| |
| 294 | +| `ICR_NAMESPACE` | **required** | Your IBM Container Registry namespace | |
| 295 | +| `CE_API_KEY` | **required** | IBM Cloud API key (Code Engine Writer + COS Reader — fleet API and COS) | |
| 296 | +| `NAME_PREFIX` | `interactive-fleets` | Prefix for all created resource names | |
| 297 | +| `FLEET_IMAGE` | `private.de.icr.io/<ICR_NAMESPACE>/interactive-fleets-task:latest` | Task image | |
| 298 | +| `FLEET_REGISTRY_SECRET` | `ce-auto-icr-private-eu-de` | Registry pull secret | |
| 299 | +| `FLEET_MAX_SCALE` | `2` | Maximum worker nodes | |
| 300 | +| `FLEET_SCALE_DOWN_DELAY` | `300` | Idle seconds before scaling down (5 min) | |
| 301 | +| `FLEET_CPU` | `1` | vCPUs per task container | |
| 302 | +| `FLEET_MEMORY` | `2G` | Memory per task container | |
| 303 | +| `FN_NAME` | `interactive-fleet-fn` | CE Function name | |
| 304 | +| `SECRET_NAME` | `interactive-fleet-secrets` | Secret holding `CE_API_KEY` | |
| 305 | +| `COS_PREFIX` | `interactive-fleet-result` | Folder prefix in COS bucket for results | |
| 306 | +| `POLL_TIMEOUT` | `30` | Max poll seconds (function env var) | |
| 307 | +
|
| 308 | +--- |
| 309 | +
|
| 310 | +## Customising the workload |
| 311 | +
|
| 312 | +The enrichment logic lives entirely in [`task-container/entrypoint.sh`](task-container/entrypoint.sh) |
| 313 | +in the enrichment section (steps 3–6). Replace it with any processing you need: |
| 314 | +
|
| 315 | +- ML model inference |
| 316 | +- External API calls |
| 317 | +- File or document transformation |
| 318 | +- Database writes |
| 319 | +
|
| 320 | +The contract the function and worker share: |
| 321 | +1. The function submits one task per request with: |
| 322 | + ```json |
| 323 | + { "idx": "<task_id>", "args": ["<json-string-of-input>"] } |
| 324 | + ``` |
| 325 | +2. The worker receives the JSON string as `$1` (or `sys.argv[1]` for Python) and reads it directly — no key-value parsing needed. |
| 326 | +3. The worker writes the result JSON to `{RESULTS_DIR}/interactive-fleet-result/{task_id}.json`. |
| 327 | +
|
| 328 | +After changing the worker, rebuild and redeploy with: |
| 329 | +
|
| 330 | +```bash |
| 331 | +./deploy |
| 332 | +# or, to skip fleet re-creation: |
| 333 | +SKIP_FLEET=1 FLEET_ID=<existing-id> ./deploy |
| 334 | +``` |
0 commit comments