Skip to content

Commit 1f81725

Browse files
committed
Initial public commit
0 parents  commit 1f81725

18 files changed

Lines changed: 1094 additions & 0 deletions

File tree

.cache/bin/fc-init

1.63 MB
Binary file not shown.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: Build rootfs images
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "tenants/**"
8+
- "configs/**"
9+
- "scripts/**"
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-24.04-arm
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Setup Go
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version: "1.25.x"
22+
23+
- name: Install dependencies
24+
run: |
25+
sudo apt-get update -qq
26+
sudo apt-get install -y -qq jq e2fsprogs
27+
28+
- name: Install yq
29+
run: |
30+
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64
31+
sudo chmod +x /usr/local/bin/yq
32+
33+
- name: Resolve fc-init
34+
env:
35+
FC_INIT_VERSION: ${{ vars.FC_INIT_VERSION }}
36+
FIREWORK_GITHUB_TOKEN: ${{ secrets.FIREWORK_GITHUB_TOKEN }}
37+
run: |
38+
set -euo pipefail
39+
mkdir -p .cache/bin
40+
41+
if [ -n "${FC_INIT_VERSION:-}" ]; then
42+
tag="${FC_INIT_VERSION#v}"
43+
tag="v${tag}"
44+
url="https://github.com/artemnikitin/firework/releases/download/${tag}/fc-init-linux-arm64"
45+
echo "Downloading fc-init from release ${tag}"
46+
curl -fsSL "$url" -o .cache/bin/fc-init
47+
chmod +x .cache/bin/fc-init
48+
else
49+
echo "FC_INIT_VERSION not set; building fc-init from firework@main"
50+
if [ -n "${FIREWORK_GITHUB_TOKEN:-}" ]; then
51+
git config --global url."https://x-access-token:${FIREWORK_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
52+
export GOPRIVATE=github.com/artemnikitin/*
53+
fi
54+
55+
if ! GOBIN="$PWD/.cache/bin" GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
56+
go install github.com/artemnikitin/firework/cmd/fc-init@main; then
57+
echo "::warning::Failed to build fc-init from firework@main. Falling back to bundled source."
58+
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
59+
go build -ldflags "-s -w" -o .cache/bin/fc-init ./scripts/fc-init/main.go
60+
fi
61+
fi
62+
63+
- name: Build per-tenant rootfs images
64+
run: |
65+
chmod +x scripts/docker-to-rootfs.sh
66+
67+
for tenant_dir in tenants/*/; do
68+
[ -d "$tenant_dir" ] || continue
69+
tenant_id="$(basename "$tenant_dir")"
70+
echo "::group::Tenant: $tenant_id"
71+
72+
for svc_file in "${tenant_dir}"*.yaml "${tenant_dir}"*.yml; do
73+
[ -f "$svc_file" ] || continue
74+
base_name="$(basename "${svc_file%.*}")" # e.g. "kibana"
75+
76+
source_image="$(yq '.source_image // ""' "$svc_file")"
77+
if [ -z "$source_image" ]; then
78+
echo "Skipping ${tenant_id}-${base_name} — no source_image"
79+
continue
80+
fi
81+
82+
size_mb="$(yq '.rootfs_size_mb // 512' "$svc_file")"
83+
output="${tenant_id}-${base_name}-rootfs.ext4"
84+
85+
# Config overlay: tenant-specific overlay takes precedence.
86+
overlay_arg=""
87+
if [ -d "configs/${tenant_id}-${base_name}" ]; then
88+
overlay_arg="configs/${tenant_id}-${base_name}"
89+
elif [ -d "configs/${base_name}" ]; then
90+
overlay_arg="configs/${base_name}"
91+
fi
92+
93+
echo "Building $output from $source_image"
94+
./scripts/docker-to-rootfs.sh "$source_image" "$output" "$size_mb" \
95+
"${overlay_arg:-}" ".cache/bin/fc-init"
96+
done
97+
echo "::endgroup::"
98+
done
99+
100+
- name: Upload images to S3
101+
env:
102+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
103+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
104+
AWS_REGION: ${{ vars.AWS_REGION }}
105+
S3_IMAGES_BUCKET: ${{ vars.S3_IMAGES_BUCKET }}
106+
run: |
107+
for ext4 in *-rootfs.ext4; do
108+
[ -f "$ext4" ] || continue
109+
echo "Uploading $ext4 to s3://${S3_IMAGES_BUCKET}/${ext4}"
110+
aws s3 cp "$ext4" "s3://${S3_IMAGES_BUCKET}/${ext4}"
111+
done

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.idea/
2+
.DS_Store
3+
*.ext4

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Artem Nikitin
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Elasticsearch configuration — MVP single-node deployment.
2+
# This file is overlaid into the rootfs at build time by docker-to-rootfs.sh.
3+
4+
cluster.name: "firework"
5+
node.name: "es-01"
6+
7+
# Listen on all interfaces so other microVMs on the bridge can connect.
8+
network.host: 0.0.0.0
9+
10+
# Single-node discovery (no cluster formation).
11+
discovery.type: single-node
12+
13+
# Disable security for MVP simplicity.
14+
# For production, enable security and configure TLS.
15+
xpack.security.enabled: false
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Kibana configuration — MVP deployment.
2+
# This file is overlaid into the rootfs at build time by docker-to-rootfs.sh.
3+
4+
# Listen on all interfaces so the host can proxy traffic from the ALB.
5+
server.host: "0.0.0.0"
6+
server.name: "kibana"
7+
8+
# Elasticsearch connection.
9+
# ELASTICSEARCH_HOSTS is injected at runtime by the firework agent via
10+
# service links (see services/kibana.yaml). No need to hardcode IPs.
11+
elasticsearch.hosts: ["${ELASTICSEARCH_HOSTS}"]
12+
13+
# NOTE: Kibana 9 validates settings strictly; avoid deprecated/removed keys.
14+
# For production, configure security and TLS explicitly via supported options.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Elasticsearch configuration — tenant-1.
2+
# This file is overlaid into the rootfs at build time by docker-to-rootfs.sh.
3+
4+
cluster.name: "firework-tenant-1"
5+
node.name: "es-tenant-1"
6+
7+
network.host: 0.0.0.0
8+
discovery.type: single-node
9+
xpack.security.enabled: false
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Elasticsearch configuration — tenant-2.
2+
# This file is overlaid into the rootfs at build time by docker-to-rootfs.sh.
3+
4+
cluster.name: "firework-tenant-2"
5+
node.name: "es-tenant-2"
6+
7+
network.host: 0.0.0.0
8+
discovery.type: single-node
9+
xpack.security.enabled: false

defaults.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Global defaults applied to every service unless overridden.
2+
# The enricher fills these in for any field the service definition omits.
3+
kernel: "/var/lib/images/vmlinux-5.10"
4+
vcpus: 1
5+
memory_mb: 256
6+
kernel_args: "console=ttyS0 reboot=k panic=1 pci=off init=/sbin/fc-init"
7+
health_check:
8+
type: "http"
9+
port: 8080
10+
path: "/health"
11+
interval: "15s"
12+
timeout: "5s"
13+
retries: 3

0 commit comments

Comments
 (0)