Feast feature store
Feast feature store
This validated OpenTofu template composes Compute, Network, and Block Storage into a self-hosted ML feature store you run on infrastructure you control.
What this template does#
Provisions a single instance running Feast, an open-source feature store for training and serving ML features:
- Docker stack with a Feast feature server on port 6566, plus bundled Postgres (offline store and SQL registry) and Redis (online store) when
store_modeisbundled - Default sizing:
s1a.small(2 vCPU / 2 GiB RAM) and a 30 GiB data volume at/var/lib/dockerfor container layers and database files - Private network, security group, floating IP; feature server port 6566 restricted to
server_allowed_cidrby default - Sample
driver_hourly_statsfeature view and seed rows in Postgres for smoke tests after first boot
Feast keeps offline training data, online serving lookups, and registry metadata in stores you operate. Point store_mode at external and set postgres_host and redis_host to wire in self-managed PostgreSQL and Redis cache instead of the bundled containers.
No credential ships with this template. cloud-init generates a Postgres password on first boot and writes it to /opt/feast/.env on the instance. For external stores, add FEAST_DB_PASSWORD there after apply.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist) | No default |
flavor_name | Instance size (bundled Postgres + Redis on 2 vCPU / 2 GiB) | s1a.small |
image_name | Operating system image | Ubuntu-24.04 |
app_name | Display name prefix for resources | feast |
volume_size | Block volume size in GiB, mounted at /var/lib/docker | 30 |
external_network | External network for floating IP allocation | PublicStatic |
private_cidr | CIDR for the private subnet | 10.55.0.0/24 |
server_allowed_cidr | CIDR allowed to reach the feature server on port 6566 | 10.55.0.0/24 |
store_mode | bundled (Postgres + Redis on this VM) or external | bundled |
postgres_host | PostgreSQL host when store_mode is external | "" |
redis_host | Redis host when store_mode is external | "" |
postgres_port | PostgreSQL port when store_mode is external | 5432 |
postgres_database | PostgreSQL database when store_mode is external | feast |
postgres_user | PostgreSQL user when store_mode is external | feast |
redis_port | Redis port when store_mode is external | 6379 |
Feature server access and security#
The Feast feature server listens on port 6566 (gRPC). The security group restricts 6566 to server_allowed_cidr, which defaults to the private network only. Reach the server over an SSH tunnel, through a reverse proxy on 443, or by setting server_allowed_cidr to YOUR_IP/32.
Ports 80 and 443 stay open for a reverse proxy you add in front of the server; they carry no traffic until you configure one.
Store modes#
The store_mode parameter selects where Feast keeps data:
bundled(default): Postgres holds the offline store, SQL registry, and sampledriver_statstable; Redis holds the online store. Both run as containers on the data volume.external: only the Feast server containers run on this VM. Setpostgres_hostandredis_hostto private IPs from your Postgres and Redis stacks, create thefeastdatabase anddriver_statstable on Postgres, then addFEAST_DB_PASSWORDto/opt/feast/.envand rundocker compose up -din/opt/feast.
When to use this pattern#
Run a feature store that serves low-latency online features to inference workloads and materializes historical features from Postgres for training. Feast suits the feature layer in a notebook workbench or pipeline stack alongside experiment tracking and a warehouse.
For the warehouse itself, see self-managed PostgreSQL. For the online cache layer, see Redis cache.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared vCPU.
Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.
What each resource is for
Feast
s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
s1a.small
2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
2 vCPU + 2 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (60 GiB)
60 GiB at $0.08/GiB/mo
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
Included at no charge
These line items are zero on Quake AI. Many other providers meter them separately.
Data transfer (inbound and outbound)
Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.
AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.
Learn morePrivate networking
Private networks, subnets, Neutron routers, and security groups are included with the plan.
VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.
Control-plane API requests
OpenStack API calls for provisioning and management are included.
Some managed services on other clouds meter API calls or charge for premium control-plane features.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
Show source (6 files)Hide source
data "openstack_images_image_v2" "os" {
name = var.image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_network_v2" "private" {
name = "${var.app_name}-net"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "${var.app_name}-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = var.private_cidr
ip_version = 4
dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}
resource "openstack_networking_router_v2" "main" {
name = "${var.app_name}-router"
external_network_id = data.openstack_networking_network_v2.external.id
}
resource "openstack_networking_router_interface_v2" "private" {
router_id = openstack_networking_router_v2.main.id
subnet_id = openstack_networking_subnet_v2.private.id
}
resource "openstack_networking_secgroup_v2" "feast" {
name = "${var.app_name}-sg"
description = "SSH and HTTP/HTTPS for a reverse proxy; feature server port 6566 restricted"
}
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.feast.id
}
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.feast.id
}
resource "openstack_networking_secgroup_rule_v2" "https" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.feast.id
}
resource "openstack_networking_secgroup_rule_v2" "feature_server" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 6566
port_range_max = 6566
remote_ip_prefix = var.server_allowed_cidr
security_group_id = openstack_networking_secgroup_v2.feast.id
}
resource "openstack_networking_port_v2" "feast" {
name = "${var.app_name}-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.feast.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.private.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_blockstorage_volume_v3" "data" {
name = "${var.app_name}-data"
size = var.volume_size
}
resource "openstack_compute_instance_v2" "feast" {
name = var.app_name
flavor_name = var.flavor_name
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/feast.yaml.tftpl", {
app_name = var.app_name
feast_project = replace(var.app_name, "-", "_")
store_mode = var.store_mode
postgres_host = var.postgres_host
postgres_port = var.postgres_port
postgres_database = var.postgres_database
postgres_user = var.postgres_user
redis_host = var.redis_host
redis_port = var.redis_port
})
block_device {
uuid = data.openstack_images_image_v2.os.id
source_type = "image"
destination_type = "volume"
volume_size = 30
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.feast.id
}
}
resource "openstack_compute_volume_attach_v2" "data" {
instance_id = openstack_compute_instance_v2.feast.id
volume_id = openstack_blockstorage_volume_v3.data.id
}
resource "openstack_networking_floatingip_v2" "feast" {
pool = var.external_network
}
resource "openstack_networking_floatingip_associate_v2" "feast" {
floating_ip = openstack_networking_floatingip_v2.feast.address
port_id = openstack_networking_port_v2.feast.id
}
variable "key_name" {
description = "SSH keypair name (must already exist in your project)"
type = string
}
variable "flavor_name" {
description = "Instance size. Feast with bundled Postgres and Redis runs on 2 vCPU and 2 GiB RAM."
type = string
default = "s1a.small"
}
variable "image_name" {
description = "Operating system image. Ubuntu 24.04 is the recommended base."
type = string
default = "Ubuntu-24.04"
}
variable "app_name" {
description = "Display name prefix for compute and network resources"
type = string
default = "feast"
}
variable "volume_size" {
description = "Block volume size in GiB, mounted at /var/lib/docker so registry data, Postgres, and Redis volumes live on storage you can grow."
type = number
default = 30
}
variable "external_network" {
description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
type = string
default = "PublicStatic"
}
variable "private_cidr" {
description = "CIDR for the private tenant network the instance lives in"
type = string
default = "10.55.0.0/24"
}
variable "server_allowed_cidr" {
description = "CIDR allowed to reach the Feast feature server on port 6566. Defaults to the private network only. Reach it over an SSH tunnel, or put a reverse proxy in front. To allow direct access from your workstation, set this to YOUR_IP/32."
type = string
default = "10.55.0.0/24"
}
variable "store_mode" {
description = "Feature store backing services. bundled runs Postgres and Redis in Docker on this VM. external points Feast at existing Postgres and Redis hosts (for example self-managed-postgres and redis-cache templates)."
type = string
default = "bundled"
validation {
condition = contains(["bundled", "external"], var.store_mode)
error_message = "store_mode must be either \"bundled\" or \"external\"."
}
}
variable "postgres_host" {
description = "PostgreSQL host for store_mode = external (offline store and SQL registry). Ignored when store_mode is bundled."
type = string
default = ""
}
variable "postgres_port" {
description = "PostgreSQL port for store_mode = external."
type = number
default = 5432
}
variable "postgres_database" {
description = "PostgreSQL database for store_mode = external."
type = string
default = "feast"
}
variable "postgres_user" {
description = "PostgreSQL user for store_mode = external. Add FEAST_DB_PASSWORD to /opt/feast/.env on the instance; never in tfvars."
type = string
default = "feast"
}
variable "redis_host" {
description = "Redis host for store_mode = external. Ignored when store_mode is bundled."
type = string
default = ""
}
variable "redis_port" {
description = "Redis port for store_mode = external."
type = number
default = 6379
}
output "instance_id" {
description = "ID of the compute instance running Feast"
value = openstack_compute_instance_v2.feast.id
}
output "floating_ip" {
description = "Public floating IP address of the Feast host"
value = openstack_networking_floatingip_v2.feast.address
}
output "private_ip" {
description = "Private IP address of the instance"
value = openstack_compute_instance_v2.feast.access_ip_v4
}
output "feature_server_url" {
description = "Feast feature server endpoint on port 6566. Reachable from server_allowed_cidr (the private network by default)."
value = "${openstack_networking_floatingip_v2.feast.address}:6566"
}
output "store_mode" {
description = "Whether Postgres and Redis run bundled on this VM or point at external hosts"
value = var.store_mode
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"
# Recommended: restrict the feature server (port 6566) to your workstation IP.
# Leave unset to keep 6566 reachable only from the private network and tunnel
# over SSH, or put a reverse proxy in front.
# server_allowed_cidr = "203.0.113.10/32"
# Store mode: bundled (default, Postgres + Redis in Docker) or external.
# For external, set postgres_host and redis_host to private IPs from the
# self-managed-postgres and redis-cache templates, then add FEAST_DB_PASSWORD
# to /opt/feast/.env on the instance after apply.
# store_mode = "external"
# postgres_host = "10.55.0.12"
# postgres_database = "feast"
# postgres_user = "feast"
# redis_host = "10.55.0.13"
# flavor_name = "s1a.small"
# image_name = "Ubuntu-24.04"
# app_name = "feast"
# volume_size = 30
# external_network = "PublicStatic"
# private_cidr = "10.55.0.0/24"
#cloud-config
package_update: true
packages:
- ca-certificates
- curl
- openssl
write_files:
- path: /opt/feast/docker-compose.yml
permissions: "0644"
content: |
services:
%{ if store_mode == "bundled" ~}
postgres:
image: postgres:15
restart: unless-stopped
environment:
POSTGRES_USER: feast
POSTGRES_PASSWORD: $${FEAST_DB_PASSWORD}
POSTGRES_DB: feast
volumes:
- postgres_data:/var/lib/postgresql/data
- /opt/feast/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks: [feast-net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U feast -d feast"]
interval: 5s
timeout: 5s
retries: 12
redis:
image: redis:7-alpine
restart: unless-stopped
networks: [feast-net]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 12
%{ endif ~}
feast-init:
image: python:3.11-slim
working_dir: /feature_repo
volumes:
- /opt/feast/feature_repo:/feature_repo
env_file:
- /opt/feast/.env
entrypoint: /bin/bash
command:
- -c
- |
set -e
pip install -q 'feast[postgres,redis]==0.47.0' psycopg2-binary
feast apply
feast materialize $$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%S) $$(date -u +%Y-%m-%dT%H:%M:%S)
networks: [feast-net]
%{ if store_mode == "bundled" ~}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
%{ endif ~}
feast-server:
image: python:3.11-slim
restart: unless-stopped
working_dir: /feature_repo
ports:
- "6566:6566"
volumes:
- /opt/feast/feature_repo:/feature_repo
env_file:
- /opt/feast/.env
command:
- bash
- -c
- |
set -e
pip install -q 'feast[postgres,redis]==0.47.0' psycopg2-binary
exec feast serve -h 0.0.0.0 -p 6566
networks: [feast-net]
depends_on:
feast-init:
condition: service_completed_successfully
volumes:
postgres_data: {}
networks:
feast-net:
driver: bridge
- path: /opt/feast/init.sql
permissions: "0644"
content: |
CREATE TABLE IF NOT EXISTS driver_stats (
driver_id INTEGER NOT NULL,
event_timestamp TIMESTAMP NOT NULL,
created TIMESTAMP NOT NULL DEFAULT NOW(),
conv_rate REAL,
acc_rate REAL,
avg_daily_trips INTEGER
);
INSERT INTO driver_stats (driver_id, event_timestamp, created, conv_rate, acc_rate, avg_daily_trips)
VALUES
(1001, NOW() - INTERVAL '1 hour', NOW(), 0.77, 0.91, 320),
(1002, NOW() - INTERVAL '2 hours', NOW(), 0.84, 0.88, 280),
(1003, NOW() - INTERVAL '30 minutes', NOW(), 0.73, 0.95, 410);
- path: /opt/feast/feature_repo/features.py
permissions: "0644"
content: |
from datetime import timedelta
from feast import Entity, FeatureView, Field
from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import (
PostgreSQLSource,
)
from feast.types import Float32, Int64
driver = Entity(name="driver", join_keys=["driver_id"])
driver_stats_source = PostgreSQLSource(
name="driver_stats",
query=(
"SELECT driver_id, event_timestamp, created, conv_rate, acc_rate, "
"avg_daily_trips FROM driver_stats"
),
timestamp_field="event_timestamp",
created_timestamp_column="created",
)
driver_hourly_stats = FeatureView(
name="driver_hourly_stats",
entities=[driver],
ttl=timedelta(hours=24),
schema=[
Field(name="conv_rate", dtype=Float32),
Field(name="acc_rate", dtype=Float32),
Field(name="avg_daily_trips", dtype=Int64),
],
source=driver_stats_source,
)
runcmd:
- |
set -eu
DEV=/dev/sdb
for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L feastdata "$DEV"; fi
mkdir -p /var/lib/docker
mount "$DEV" /var/lib/docker
grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/docker ext4 defaults,nofail 0 2" >> /etc/fstab
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break; sleep 5; done
STORE_MODE="${store_mode}"
POSTGRES_HOST="${postgres_host}"
POSTGRES_PORT="${postgres_port}"
POSTGRES_DATABASE="${postgres_database}"
POSTGRES_USER="${postgres_user}"
REDIS_HOST="${redis_host}"
REDIS_PORT="${redis_port}"
if [ -f /opt/feast/.env ] && grep -q '^FEAST_DB_PASSWORD=' /opt/feast/.env; then
FEAST_DB_PASSWORD="$(grep '^FEAST_DB_PASSWORD=' /opt/feast/.env | cut -d= -f2-)"
else
FEAST_DB_PASSWORD="$(openssl rand -hex 24)"
umask 077
printf 'FEAST_DB_PASSWORD=%s\n' "$FEAST_DB_PASSWORD" > /opt/feast/.env
fi
if [ "$STORE_MODE" = "bundled" ]; then
PG_HOST="postgres"
PG_PORT="5432"
PG_DB="feast"
PG_USER="feast"
REDIS_CONN="redis:6379,db=0"
else
PG_HOST="$POSTGRES_HOST"
PG_PORT="$POSTGRES_PORT"
PG_DB="$POSTGRES_DATABASE"
PG_USER="$POSTGRES_USER"
REDIS_CONN="$REDIS_HOST:$REDIS_PORT,db=0"
fi
install -d -m 0755 /opt/feast/feature_repo
{
printf '%s\n' "project: ${feast_project}"
printf '%s\n' "registry:"
printf '%s\n' " registry_type: sql"
printf '%s\n' " path: postgresql+psycopg2://$${PG_USER}:$${FEAST_DB_PASSWORD}@$${PG_HOST}:$${PG_PORT}/$${PG_DB}?sslmode=disable"
printf '%s\n' "provider: local"
printf '%s\n' "offline_store:"
printf '%s\n' " type: postgres"
printf '%s\n' " host: $${PG_HOST}"
printf '%s\n' " port: $${PG_PORT}"
printf '%s\n' " database: $${PG_DB}"
printf '%s\n' " db_schema: public"
printf '%s\n' " user: $${PG_USER}"
printf '%s\n' " password: $${FEAST_DB_PASSWORD}"
printf '%s\n' " sslmode: disable"
printf '%s\n' "online_store:"
printf '%s\n' " type: redis"
printf '%s\n' " connection_string: $${REDIS_CONN}"
printf '%s\n' "entity_key_serialization_version: 2"
} > /opt/feast/feature_repo/feature_store.yaml
cd /opt/feast
if [ "$STORE_MODE" = "bundled" ]; then
docker compose up -d postgres redis
for i in $(seq 1 60); do
docker compose exec -T postgres pg_isready -U feast -d feast >/dev/null 2>&1 && break
sleep 5
done
fi
docker compose up -d --wait feast-init
init_ec=$?
docker compose logs feast-init 2>&1 | tail -20 || true
if [ "$init_ec" -eq 0 ]; then
docker compose up -d --no-deps feast-server
else
echo "feast-init exit $${init_ec}; skipping feast-server" >&2
fi
Resources, parameters, and variables
key_namerequiredflavor_name="s1a.small"image_name="Ubuntu-24.04"app_name="feast"volume_size=30external_network="PublicStatic"private_cidr="10.55.0.0/24"server_allowed_cidr="10.55.0.0/24"store_mode="bundled" validation {postgres_host=""postgres_port=5432postgres_database="feast"postgres_user="feast"redis_host=""redis_port=6379
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 07.07.2026
See Also
Terraform and OpenTofu on Quake AI
Prerequisite
Networks
Prerequisite
Authoring IaC templates for Quake AI
Shares: Volumes, Security Groups
Deploy an API gateway with the api-gateway template
Shares: Volumes, Security Groups
Deploy a regional edge cache with the edge-cache template
Shares: Volumes, Security Groups