This is an example deployment intended for demonstration and learning purposes only. It is not hardened, audited, etc.
Example AWS and GCP deployments of Firework using Packer and Terraform.
- firework — The orchestrator itself
- firework-gitops-example — Example GitOps configuration repository
flowchart LR
GitHub["GitHub (config repo)"] -->|push webhook| EventsALB["Events ALB :443"]
CI["CI pipeline"] -->|build + upload rootfs| S3Images["S3 images bucket"]
subgraph ControlPlaneVPC["Control-plane VPC"]
direction LR
subgraph ControlPlane["Control plane (ECS/Fargate, one service by default)"]
EventsALB --> All["all-role control-plane service<br/>events + registry + controller + API"]
RegistryNLB["Registry NLB :9443"] --> All
All --> S3Configs["S3 configs/state bucket"]
end
end
subgraph DataPlaneVPC["Data-plane VPC"]
direction LR
subgraph Public["Public subnets"]
ALB["ALB :443 (HTTPS)"]
end
subgraph Nodes["Node subnet (public by default)"]
Node["c8i.2xlarge node<br/>firework-agent + Traefik"]
VM1["tenant-1-kibana VM :5611"]
VM2["tenant-1-elasticsearch VM :9200"]
VM3["tenant-2-kibana VM :5612"]
VM4["tenant-2-elasticsearch VM :9200"]
Node --> VM1
Node --> VM2
Node --> VM3
Node --> VM4
end
ALB -->|tenant traffic| Node
end
S3Configs -->|poll configs| Node
S3Images -->|download rootfs| Node
Node -->|public bootstrap-token mTLS| RegistryNLB
- Choose a provider and configure the permissions described in
iam-policies/<provider>. - Build the node image with
packer/<provider>. - Deploy
terraform/control-plane/<provider>. - Deploy
terraform/data-plane/<provider>. - Push configs/images and let the agent reconcile microVMs.
- AWS: Packer, control plane, data plane
- GCP: Packer, control plane, data plane
- Deployment status UI and CLI access: deployment-status.md
- Deploy order matters: control-plane first, data-plane second.
- On AWS, nodes are in public subnets by default so the stack creates no NAT
gateways. This is a demo-oriented cost default with real security trade-offs —
see terraform/data-plane/aws/README.md
and set
node_network_placement = "private"for anything beyond a demo. - AWS nodes use x86_64 instances with nested virtualization rather than bare metal, which is roughly six times cheaper per hour. Bare-metal Graviton remains supported.
- Node access always goes through AWS Session Manager — no SSH exposed.
- ALB serves HTTPS (TLS 1.2/1.3); host-based routing per tenant is handled by Traefik on the nodes.
- Each platform's domain variable is the single source of truth for DNS, the wildcard TLS certificate, and the agent
ingress_domain. GitOps services setmetadata.subdomain: <label>and resolve to<subdomain>.<domain>—<subdomain>.<domain_name>on AWS and<subdomain>.<base_domain>on GCP — so one provider-neutral GitOps tree serves both. - AWS node enrollment uses a shared bootstrap token stored in Secrets Manager; AWS IID-based enrollment is a known gap in this demo.
- Observability is managed as code in Terraform (dashboards, logs, access logs, metric filters).
- Each provider uses separate public origins:
events.<domain>exposes only the exact webhook path, whilestatus.<domain>exposes the authenticated deployment API and same-origin UI. AWS split mode uses a read-only S3 task role for the API; the default combined service shares the control-plane task role. GCP uses a read-only GCS Workload Identity for the status service in split mode; the default combined service shares the control-plane identity.
For GCP, the control plane runs by default as one all-role GKE Autopilot
Deployment with a shared Workload Identity; set controlplane_service_mode = "split" for one Deployment and identity per role. The events webhook and
authenticated API/UI use distinct hostnames on one GKE Gateway, backed by
durable Certificate Manager DNS-authorized certificates and a shared static IP
from terraform/events-edge/gcp. The registry is an internal TCP LoadBalancer.
The default combined pod deliberately shares the mutating control-plane identity
needed by events, registry, and controller; use split mode when the public API/UI
must retain its read-only identity. The data-plane VPC peers with the control-plane
VPC and uses Private Google Access, so its private regional managed instance group
needs no Cloud NAT or public node IPs by default. Set enable_cloud_nat = true
when guest workloads or node debugging require public internet egress. Terraform
state is local in the example stacks. The GCP control-plane registry allowlist
defaults to private 10.0.0.0/8, so the standard non-overlapping network CIDRs
work without CIDR entries in either tfvars file; set an explicit narrower
registry_allowed_cidrs list when required. See the GCP guides above for
container image, DNS delegation, private peering, and TLS prerequisites.
Destroy each provider in reverse order. AWS stacks can be destroyed in a single step. GCP requires two steps for the control-plane because the state bucket has versioning enabled and force_destroy defaults to false:
# AWS — clean teardown deletes retained local disks and orphaned bindings
./scripts/destroy-aws-data-plane.sh \
--region us-east-1 \
--project-name firework-demo \
-- -auto-approve
cd terraform/control-plane/aws && terraform destroy
# GCP — clean teardown deletes retained local disks and orphaned bindings
./scripts/destroy-gcp-data-plane.sh \
--project example-project \
--deployment-name firework \
-- -auto-approve
cd terraform/control-plane/gcp
terraform apply -var='state_bucket_force_destroy=true' -target=google_storage_bucket.state
terraform destroy -var='state_bucket_force_destroy=true'The data-plane wrappers capture the control-plane state bucket before destroy,
then remove only local-volume records whose bound cloud node no longer exists.
Use raw terraform destroy instead when retained disks and their fail-closed
bindings must be preserved for explicit recovery.