|
| 1 | +# firework-gitops-example |
| 2 | + |
| 3 | +Example GitOps configuration repository for [Firework](https://github.com/artemnikitin/firework). Demonstrates how to go from Docker images to Firecracker-bootable ext4 rootfs images in CI, and how to structure service definitions for the enricher. |
| 4 | + |
| 5 | +## Related repositories |
| 6 | + |
| 7 | +- [firework](https://github.com/artemnikitin/firework) — The orchestrator itself (agent + enricher) |
| 8 | +- [firework-deployment-example](https://github.com/artemnikitin/firework-deployment-example) — Terraform + Packer setup for deploying on AWS |
| 9 | + |
| 10 | +## Structure |
| 11 | + |
| 12 | +``` |
| 13 | +defaults.yaml # Global defaults (kernel, resources, health checks) |
| 14 | +services/ |
| 15 | + kibana.yaml # Kibana — analytics and visualization UI (v9.3.0) |
| 16 | + elasticsearch.yaml # Elasticsearch — search and analytics engine (v9.3.0) |
| 17 | +configs/ |
| 18 | + elasticsearch/ # Config overlay for Elasticsearch (mirrors guest fs layout) |
| 19 | + usr/share/elasticsearch/config/elasticsearch.yml |
| 20 | + kibana/ # Config overlay for Kibana |
| 21 | + usr/share/kibana/config/kibana.yml |
| 22 | +scripts/ |
| 23 | + docker-to-rootfs.sh # Docker image → ext4 rootfs converter (with overlay support) |
| 24 | +.github/workflows/ |
| 25 | + build-images.yaml # CI pipeline: build rootfs + upload to S3 (ARM) |
| 26 | +``` |
| 27 | + |
| 28 | +## How the image pipeline works |
| 29 | + |
| 30 | +1. You push a change to `services/`, `configs/`, or `scripts/` on `main`. |
| 31 | +2. The CI workflow (runs on ARM) reads each `services/*.yaml` file and extracts the `source_image` field. |
| 32 | +3. For each service, `scripts/docker-to-rootfs.sh` converts the Docker image to an ext4 rootfs: |
| 33 | + - `docker create` + `docker export` to extract the filesystem |
| 34 | + - `docker inspect` to read ENTRYPOINT/CMD, ENV, USER, and WORKDIR |
| 35 | + - If a `configs/<service>/` directory exists, its contents are overlaid into the rootfs (mirroring the guest filesystem layout) |
| 36 | + - Resolves `fc-init` in CI: |
| 37 | + - if `FC_INIT_VERSION` is set -> downloads `fc-init-linux-arm64` from `https://github.com/artemnikitin/firework/releases` |
| 38 | + - if `FC_INIT_VERSION` is empty -> tries `github.com/artemnikitin/firework/cmd/fc-init@main` (uses `FIREWORK_GITHUB_TOKEN` when needed for private access) |
| 39 | + - if `@main` build fails (for example, missing private repo access) -> falls back to bundled `scripts/fc-init/main.go` |
| 40 | + - Installs the compiled `/sbin/fc-init` binary into the guest image |
| 41 | + - Writes `/etc/firework/runtime.json` with image env/workdir/user metadata and writable path hints |
| 42 | + - Generates `/sbin/init` wrapper from Docker ENTRYPOINT/CMD + ENV metadata for compatibility |
| 43 | + - `mkfs.ext4 -d` to build the ext4 image (no sudo or mount needed) |
| 44 | +4. The ext4 images are uploaded to S3. |
| 45 | + |
| 46 | +## Config overlays |
| 47 | + |
| 48 | +Application-specific configuration files live in `configs/<service-name>/`. The directory structure mirrors the guest filesystem. For example: |
| 49 | + |
| 50 | +``` |
| 51 | +configs/elasticsearch/usr/share/elasticsearch/config/elasticsearch.yml |
| 52 | +``` |
| 53 | + |
| 54 | +This file overwrites `/usr/share/elasticsearch/config/elasticsearch.yml` inside the rootfs at build time. This is how we configure: |
| 55 | + |
| 56 | +- **Elasticsearch**: single-node discovery, listen on all interfaces, security disabled for MVP |
| 57 | +- **Kibana**: listen on all interfaces, connect to Elasticsearch via the `${ELASTICSEARCH_HOSTS}` environment variable (injected at runtime by the agent via service links) |
| 58 | + |
| 59 | +Config overlays handle static, per-application settings that don't change between deployments. For dynamic, per-deployment settings (like service endpoints), use service links or environment variables instead. |
| 60 | + |
| 61 | +## Service links |
| 62 | + |
| 63 | +Services can declare dependencies on other services. The firework agent resolves these at runtime — no hardcoded IPs are needed in config files. |
| 64 | + |
| 65 | +For example, Kibana declares a link to Elasticsearch: |
| 66 | + |
| 67 | +```yaml |
| 68 | +# services/kibana.yaml (excerpt) |
| 69 | +links: |
| 70 | + - service: "elasticsearch" |
| 71 | + env: "ELASTICSEARCH_HOSTS" |
| 72 | + port: 9200 |
| 73 | +``` |
| 74 | +
|
| 75 | +At boot time, the agent: |
| 76 | +1. Assigns deterministic guest IPs to all services (alphabetically by name, starting at `.2`) |
| 77 | +2. Resolves each link to a concrete URL (e.g. `http://172.16.0.2:9200`) |
| 78 | +3. Injects it as a kernel boot argument (`firework.env.ELASTICSEARCH_HOSTS=http://172.16.0.2:9200`) |
| 79 | +4. The guest's `fc-init` exports it as an environment variable |
| 80 | + |
| 81 | +Kibana's config overlay uses this variable: |
| 82 | + |
| 83 | +```yaml |
| 84 | +# configs/kibana/usr/share/kibana/config/kibana.yml |
| 85 | +elasticsearch.hosts: ["${ELASTICSEARCH_HOSTS}"] |
| 86 | +``` |
| 87 | + |
| 88 | +This way, if IPs change (e.g. services are reordered), no config files need updating. |
| 89 | + |
| 90 | +## Runtime environment variables |
| 91 | + |
| 92 | +In addition to build-time config overlays, the firework-agent can inject environment variables at runtime via kernel boot arguments. Service definitions can include an `env` map: |
| 93 | + |
| 94 | +```yaml |
| 95 | +env: |
| 96 | + SERVER_HOST: "0.0.0.0" |
| 97 | +``` |
| 98 | + |
| 99 | +The agent appends these as `firework.env.KEY=VALUE` entries to the kernel command line. The guest's `/sbin/fc-init` parses `/proc/cmdline` and exports them before launching the application. This is useful for settings that vary per deployment without rebuilding images. |
| 100 | + |
| 101 | +Environment variables from service links are automatically merged into the env map. |
| 102 | + |
| 103 | +## Service definitions |
| 104 | + |
| 105 | +Each file in `services/` defines a Firecracker microVM service: |
| 106 | + |
| 107 | +```yaml |
| 108 | +name: "my-service" |
| 109 | +source_image: "myorg/myapp:latest" # Docker image to convert (used by CI) |
| 110 | +image: "/var/lib/images/my-service-rootfs.ext4" # Path on the host (used by agent) |
| 111 | +rootfs_size_mb: 1024 # Rootfs image size (used by CI, default: 512) |
| 112 | +node_type: "web" # Which node group to run on |
| 113 | +vcpus: 2 |
| 114 | +memory_mb: 512 |
| 115 | +network: true |
| 116 | +links: # Inter-service dependencies |
| 117 | + - service: "other-service" |
| 118 | + env: "OTHER_SERVICE_URL" |
| 119 | + port: 8080 |
| 120 | +port_forwards: # Expose ports to the host |
| 121 | + - host_port: 80 |
| 122 | + vm_port: 8080 |
| 123 | +health_check: |
| 124 | + type: "http" |
| 125 | + port: 8080 |
| 126 | + path: "/healthz" |
| 127 | +metadata: |
| 128 | + version: "1.0.0" |
| 129 | +``` |
| 130 | + |
| 131 | +| Field | Required | Description | |
| 132 | +|---|---|---| |
| 133 | +| `name` | yes | Unique service name | |
| 134 | +| `source_image` | no | Docker image for CI rootfs build | |
| 135 | +| `image` | yes | Path to the rootfs image on the host | |
| 136 | +| `rootfs_size_mb` | no | Rootfs image size in MB (default: 512, used by CI) | |
| 137 | +| `node_type` | yes | Determines which node group runs this service | |
| 138 | +| `vcpus` | no | Virtual CPUs (falls back to `defaults.yaml`) | |
| 139 | +| `memory_mb` | no | Memory in MB (falls back to `defaults.yaml`) | |
| 140 | +| `network` | no | Whether the service needs networking | |
| 141 | +| `links` | no | Dependencies on other services (resolved by agent at runtime) | |
| 142 | +| `port_forwards` | no | Host-to-VM port mappings for external access | |
| 143 | +| `env` | no | Runtime environment variables | |
| 144 | +| `health_check` | no | Health check configuration | |
| 145 | +| `metadata` | no | Arbitrary key-value pairs | |
| 146 | + |
| 147 | +Only `name`, `image`, and `node_type` are required. Everything else falls back to `defaults.yaml`. |
| 148 | + |
| 149 | +## How it connects to Firework |
| 150 | + |
| 151 | +``` |
| 152 | + ┌──────────────┐ |
| 153 | + git push ──────► │ CI Workflow │──── ext4 images ────► S3 |
| 154 | + └──────────────┘ |
| 155 | + │ |
| 156 | + git push ──────► Enricher Lambda ── node configs ──► S3 │ |
| 157 | + │ │ |
| 158 | + ┌──────────────┐ ▼ ▼ |
| 159 | + │ Firework │◄─── polls S3 for configs + images |
| 160 | + │ Agent │ |
| 161 | + └──────────────┘ |
| 162 | +``` |
| 163 | +
|
| 164 | +1. **CI pipeline** builds ext4 rootfs images from Docker images (with config overlays) and uploads them to S3. |
| 165 | +2. **Enricher Lambda** (triggered by webhook on push) reads service definitions, applies defaults, and writes enriched per-node configs to S3. |
| 166 | +3. **Firework agents** on each node poll S3 for both configs and images, resolve service links, and converge to the desired state. |
| 167 | +
|
| 168 | +## Local testing |
| 169 | +
|
| 170 | +Test the conversion script locally: |
| 171 | +
|
| 172 | +```bash |
| 173 | +# Build fc-init once (from sibling firework repo) |
| 174 | +(cd ../firework && make build-fc-init) |
| 175 | +
|
| 176 | +# Without config overlay: |
| 177 | +./scripts/docker-to-rootfs.sh docker.elastic.co/kibana/kibana:9.3.0 /tmp/kibana.ext4 2048 "" ../firework/bin/fc-init |
| 178 | +
|
| 179 | +# With config overlay: |
| 180 | +./scripts/docker-to-rootfs.sh docker.elastic.co/kibana/kibana:9.3.0 /tmp/kibana.ext4 2048 configs/kibana ../firework/bin/fc-init |
| 181 | +
|
| 182 | +file /tmp/kibana.ext4 # should show "Linux rev 1.0 ext4 filesystem data" |
| 183 | +``` |
0 commit comments