Live RTMP/SRT ingest and restream
Live RTMP/SRT ingest and restream
This pattern composes Compute, Network, and Object Storage.
What this template deploys#
- Private network and router for the ingest instance
- One ingest VM with cloud-init that installs NGINX-RTMP (default) or SRS
- 1 floating IP on the ingest port for public RTMP (1935/tcp) and SRT (configurable UDP) pushes
- Security group rules scoped to
encoder_cidrfor ingest andadmin_cidrfor SSH - Optional Object Storage bucket for HLS replay when
enable_hls_recordingis true
Point OBS or a hardware encoder at the rtmp_publish_url output. The instance restreams to each URL in restream_targets.
Prerequisites#
- OpenTofu or Terraform >= 1.6.0
- OpenStack application credentials (
source openrc.sh) - One floating IP available in your project quota
- EC2-compatible Object Storage credentials when enabling HLS replay. See S3 storage ACL template.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist in your project) | required |
ingest_server | nginx-rtmp or srs | nginx-rtmp |
ingest_flavor | CPU flavor for ingest/restream work | c2a.large |
encoder_cidr | CIDR allowed to push RTMP/SRT | 0.0.0.0/0 |
admin_cidr | CIDR allowed for SSH | 0.0.0.0/0 |
application_name | RTMP application name | live |
stream_key | Optional publish key | "" |
restream_targets | Downstream RTMP push URLs | [] |
enable_hls_recording | Upload HLS segments to Object Storage | false |
hls_bucket_name | Replay bucket (when recording enabled) | "" |
s3_access_key / s3_secret_key | EC2-compat credentials for replay upload | required when recording |
image_name | Boot image name | Ubuntu-24.04 |
external_network | Shared external network for router gateway and floating IP | PublicStatic |
private_cidr | Private subnet CIDR for the ingest instance | 192.168.75.0/24 |
instance_name | Ingest instance display name | live-ingest |
srt_port | UDP port for SRT ingest when using srs | 10080 |
s3_endpoint | Quake AI S3-compatible endpoint URL | https://object.us-east-1.rumble.cloud |
s3_region | S3 region identifier for the AWS provider | us-east-1 |
Cost and sizing#
Size ingest_flavor for the number of simultaneous restream targets and any CPU transcoding you enable. The template allocates exactly one floating IP. Object Storage charges follow your plan allowance for stored replay segments.
When to use this pattern#
Accept a single RTMP or SRT push from OBS or a hardware encoder and fan out to YouTube, Twitch, or custom RTMP endpoints without a managed live-video SaaS. For batch audio processing, see the audio post-production worker template.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on dedicated 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
Ingest
c2a.large · 2 dedicated vCPU, 4 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
c2a.large
2 dedicated vCPU, 4 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
2 vCPU + 4 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (40 GiB)
40 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.
Dev/test vs production
Start on shared CPU for dev/test, then promote to dedicated for production with a flavor resize. The network, storage, and template stay the same.
Dev/test on shared CPU
Burstable s1a flavors; suited to prototyping and low or bursty load.
Production on dedicated CPU
The headline estimate above; predictable steady-load performance.
Saves $45.50/mo while you build on shared CPU.
Shared flavors carry less RAM (c2a.large (4 GiB RAM) -> s1a.small (2 GiB RAM)). A resize reboots the instance; data on attached volumes persists. Size the dedicated flavor for the RAM your production workload needs.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
Show source (7 files)Hide source
locals {
restream_targets_lines = length(var.restream_targets) == 0 ? "# no restream targets configured" : join("\n", var.restream_targets)
}
data "openstack_images_image_v2" "os" {
name = var.image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "aws_s3_bucket" "hls" {
count = var.enable_hls_recording ? 1 : 0
bucket = var.hls_bucket_name
}
resource "aws_s3_bucket_acl" "hls" {
count = var.enable_hls_recording ? 1 : 0
bucket = aws_s3_bucket.hls[0].id
acl = "private"
}
resource "openstack_networking_network_v2" "private" {
name = "${var.instance_name}-private"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "${var.instance_name}-private-sn"
network_id = openstack_networking_network_v2.private.id
cidr = var.private_cidr
ip_version = 4
enable_dhcp = true
dns_nameservers = ["8.8.8.8", "8.8.4.4"]
}
resource "openstack_networking_router_v2" "router" {
name = "${var.instance_name}-router"
external_network_id = data.openstack_networking_network_v2.external.id
admin_state_up = true
}
resource "openstack_networking_router_interface_v2" "private" {
router_id = openstack_networking_router_v2.router.id
subnet_id = openstack_networking_subnet_v2.private.id
}
resource "openstack_networking_secgroup_v2" "ingest" {
name = "${var.instance_name}-sg"
description = "RTMP/SRT ingest from encoder CIDR; SSH from admin CIDR"
}
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = var.admin_cidr
security_group_id = openstack_networking_secgroup_v2.ingest.id
}
resource "openstack_networking_secgroup_rule_v2" "rtmp" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 1935
port_range_max = 1935
remote_ip_prefix = var.encoder_cidr
security_group_id = openstack_networking_secgroup_v2.ingest.id
}
resource "openstack_networking_secgroup_rule_v2" "srt" {
direction = "ingress"
ethertype = "IPv4"
protocol = "udp"
port_range_min = var.srt_port
port_range_max = var.srt_port
remote_ip_prefix = var.encoder_cidr
security_group_id = openstack_networking_secgroup_v2.ingest.id
}
resource "openstack_networking_port_v2" "ingest" {
name = "${var.instance_name}-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.ingest.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.private.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_compute_instance_v2" "ingest" {
name = var.instance_name
flavor_name = var.ingest_flavor
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/ingest.yaml", {
ingest_server = var.ingest_server
application_name = var.application_name
stream_key = var.stream_key
restream_targets_lines = local.restream_targets_lines
enable_hls_recording = var.enable_hls_recording ? "true" : "false"
s3_endpoint = var.s3_endpoint
s3_region = var.s3_region
s3_access_key = var.s3_access_key
s3_secret_key = var.s3_secret_key
hls_bucket = var.enable_hls_recording ? aws_s3_bucket.hls[0].id : ""
srt_port = var.srt_port
})
block_device {
uuid = data.openstack_images_image_v2.os.id
source_type = "image"
destination_type = "volume"
volume_size = 40
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.ingest.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_networking_floatingip_v2" "ingest" {
pool = var.external_network
}
resource "openstack_networking_floatingip_associate_v2" "ingest" {
floating_ip = openstack_networking_floatingip_v2.ingest.address
port_id = openstack_networking_port_v2.ingest.id
}
variable "key_name" {
description = "Existing SSH keypair name in the project for compute instances"
type = string
}
variable "ingest_flavor" {
description = "Compute flavor for the ingest VM (CPU-only; size for concurrent FFmpeg restream jobs)"
type = string
default = "c2a.large"
}
variable "image_name" {
description = "Boot image name"
type = string
default = "Ubuntu-24.04"
}
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 = "Private subnet CIDR for the ingest instance"
type = string
default = "192.168.75.0/24"
}
variable "instance_name" {
description = "Ingest instance display name"
type = string
default = "live-ingest"
}
variable "ingest_server" {
description = "Ingest software to install: nginx-rtmp or srs"
type = string
default = "nginx-rtmp"
validation {
condition = contains(["nginx-rtmp", "srs"], var.ingest_server)
error_message = "ingest_server must be nginx-rtmp or srs."
}
}
variable "encoder_cidr" {
description = "Source CIDR allowed to push RTMP (1935/tcp) and SRT (srt_port/udp). Restrict to your encoder or studio egress IP."
type = string
default = "0.0.0.0/0"
}
variable "admin_cidr" {
description = "Source CIDR allowed for SSH (22/tcp) administration"
type = string
default = "0.0.0.0/0"
}
variable "srt_port" {
description = "UDP port opened for SRT ingest when using srs"
type = number
default = 10080
}
variable "application_name" {
description = "RTMP application name encoders push to (for example live)"
type = string
default = "live"
}
variable "stream_key" {
description = "RTMP stream key encoders must publish (empty accepts any key on nginx-rtmp)"
type = string
default = ""
sensitive = true
}
variable "restream_targets" {
description = "Downstream RTMP push URLs (one per line in cloud-init config). Use pass-through copy where possible."
type = list(string)
default = []
}
variable "enable_hls_recording" {
description = "When true, record HLS segments locally and upload to Object Storage"
type = bool
default = false
}
variable "hls_bucket_name" {
description = "Object Storage bucket for HLS replay segments (required when enable_hls_recording is true)"
type = string
default = ""
}
variable "s3_access_key" {
description = "EC2-compatible access key for Object Storage (required when enable_hls_recording is true)"
type = string
default = ""
sensitive = true
}
variable "s3_secret_key" {
description = "EC2-compatible secret key paired with s3_access_key"
type = string
default = ""
sensitive = true
}
variable "s3_endpoint" {
description = "Quake AI S3-compatible endpoint URL"
type = string
default = "https://object.us-east-1.rumble.cloud"
}
variable "s3_region" {
description = "S3 region identifier passed to the AWS provider"
type = string
default = "us-east-1"
}
output "ingest_instance_id" {
description = "Nova instance ID for the ingest server"
value = openstack_compute_instance_v2.ingest.id
}
output "ingest_floating_ip" {
description = "Public floating IP encoders push RTMP/SRT to"
value = openstack_networking_floatingip_v2.ingest.address
}
output "rtmp_publish_url" {
description = "RTMP URL for OBS or hardware encoders (append stream key if configured)"
value = "rtmp://${openstack_networking_floatingip_v2.ingest.address}/${var.application_name}"
}
output "hls_bucket" {
description = "Object Storage bucket for HLS replay (empty when recording is disabled)"
value = var.enable_hls_recording ? aws_s3_bucket.hls[0].id : ""
}
output "floating_ip_count" {
description = "Floating IPs this template allocates (one public ingest endpoint)"
value = 1
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "openstack" {}
provider "aws" {
region = var.s3_region
access_key = var.s3_access_key
secret_key = var.s3_secret_key
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
s3 = var.s3_endpoint
}
}
# Required
key_name = "YOUR_KEY_NAME"
# ingest_server = "nginx-rtmp"
# ingest_flavor = "c2a.large"
# application_name = "live"
# stream_key = "your-secret-stream-key"
# encoder_cidr = "203.0.113.10/32"
# admin_cidr = "203.0.113.10/32"
# restream_targets = [
# "rtmp://a.rtmp.youtube.com/live2/YOUR_YOUTUBE_KEY",
# "rtmp://live.twitch.tv/app/YOUR_TWITCH_KEY",
# ]
# EC2-compatible credentials from: openstack ec2 credentials create
# Required for tofu plan even when HLS recording is disabled (AWS provider init).
# s3_access_key = "YOUR_EC2_ACCESS_KEY"
# s3_secret_key = "YOUR_EC2_SECRET_KEY"
# Optional HLS replay to Object Storage
# enable_hls_recording = true
# hls_bucket_name = "my-live-hls-replay"
#cloud-config
package_update: true
package_upgrade: true
packages:
- ffmpeg
write_files:
- path: /etc/live-ingest/restream-targets.txt
permissions: "0644"
content: |
${restream_targets_lines}
- path: /etc/live-ingest/env
permissions: "0600"
content: |
INGEST_SERVER=${ingest_server}
APPLICATION_NAME=${application_name}
STREAM_KEY=${stream_key}
ENABLE_HLS_RECORDING=${enable_hls_recording}
S3_ENDPOINT=${s3_endpoint}
S3_REGION=${s3_region}
AWS_ACCESS_KEY_ID=${s3_access_key}
AWS_SECRET_ACCESS_KEY=${s3_secret_key}
HLS_BUCKET=${hls_bucket}
SRT_PORT=${srt_port}
runcmd:
- |
set -eu
apt-get install -y python3-pip
pip3 install --break-system-packages awscli
. /etc/live-ingest/env
mkdir -p /var/lib/live-ingest/hls
if [ "$INGEST_SERVER" = "srs" ]; then
apt-get install -y docker.io
systemctl enable --now docker
docker run -d --name srs --restart unless-stopped \
-p 1935:1935 -p "$SRT_PORT:$SRT_PORT/udp" \
-v /var/lib/live-ingest/hls:/usr/local/srs/objs/nginx/html \
ossrs/srs:5
else
apt-get install -y nginx libnginx-mod-rtmp
PUSH_LINES=""
while IFS= read -r target; do
case "$target" in ""|\#*) continue ;; esac
PUSH_LINES="$${PUSH_LINES} push $${target};"$'\n'
done < /etc/live-ingest/restream-targets.txt
cat > /etc/nginx/nginx.conf <<NGINX
load_module modules/ngx_rtmp_module.so;
worker_processes auto;
events { worker_connections 1024; }
rtmp {
server {
listen 1935;
chunk_size 4096;
application $${APPLICATION_NAME} {
live on;
record off;
$${PUSH_LINES} }
}
}
http {
server {
listen 8080;
location /hls { root /var/lib/live-ingest; types { application/vnd.apple.mpegurl m3u8; } }
}
}
NGINX
systemctl enable nginx
systemctl restart nginx
fi
# Live RTMP/SRT ingest and restream
CPU ingest VM with one floating IP for encoder pushes, optional downstream RTMP restream targets, and optional HLS segment upload to Object Storage.
**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
## Prerequisites
- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- One available floating IP quota slot
- EC2-compatible Object Storage credentials when `enable_hls_recording` is true
## Usage
1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`
## Variables
| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
| `ingest_flavor` | string | no | `c2a.large` | CPU flavor for the ingest VM |
| `ingest_server` | string | no | `nginx-rtmp` | `nginx-rtmp` or `srs` |
| `encoder_cidr` | string | no | `0.0.0.0/0` | CIDR allowed to push RTMP/SRT |
| `admin_cidr` | string | no | `0.0.0.0/0` | CIDR allowed for SSH |
| `restream_targets` | list(string) | no | `[]` | Downstream RTMP push URLs |
| `enable_hls_recording` | bool | no | `false` | Upload HLS segments to Object Storage |
| `hls_bucket_name` | string | when recording | `""` | Replay bucket name |
## Documentation
Full documentation: [Live ingest and restream template](/docs/automation/templates/live-ingest-restream)
Variations
Deploy walkthroughValidated variants of this template, each adding one capability the base does not include.
- Object Storage
live-ingest-restream-archiveDedicated Object Storage archive bucket for long-term recordings
Show source and downloadHide source
7 files. Download the zip or copy any file.Download live-ingest-restream-archive.zipmain.tfHCLlocals { restream_targets_lines = length(var.restream_targets) == 0 ? "# no restream targets configured" : join("\n", var.restream_targets) } data "openstack_images_image_v2" "os" { name = var.image_name most_recent = true } data "openstack_networking_network_v2" "external" { name = var.external_network } resource "aws_s3_bucket" "hls" { count = var.enable_hls_recording ? 1 : 0 bucket = var.hls_bucket_name } resource "aws_s3_bucket_acl" "hls" { count = var.enable_hls_recording ? 1 : 0 bucket = aws_s3_bucket.hls[0].id acl = "private" } resource "aws_s3_bucket" "archive" { bucket = var.archive_bucket_name } resource "aws_s3_bucket_acl" "archive" { bucket = aws_s3_bucket.archive.id acl = "private" } resource "openstack_networking_network_v2" "private" { name = "${var.instance_name}-private" admin_state_up = true } resource "openstack_networking_subnet_v2" "private" { name = "${var.instance_name}-private-sn" network_id = openstack_networking_network_v2.private.id cidr = var.private_cidr ip_version = 4 enable_dhcp = true dns_nameservers = ["8.8.8.8", "8.8.4.4"] } resource "openstack_networking_router_v2" "router" { name = "${var.instance_name}-router" external_network_id = data.openstack_networking_network_v2.external.id admin_state_up = true } resource "openstack_networking_router_interface_v2" "private" { router_id = openstack_networking_router_v2.router.id subnet_id = openstack_networking_subnet_v2.private.id } resource "openstack_networking_secgroup_v2" "ingest" { name = "${var.instance_name}-sg" description = "RTMP/SRT ingest from encoder CIDR; SSH from admin CIDR" } resource "openstack_networking_secgroup_rule_v2" "ssh" { direction = "ingress" ethertype = "IPv4" protocol = "tcp" port_range_min = 22 port_range_max = 22 remote_ip_prefix = var.admin_cidr security_group_id = openstack_networking_secgroup_v2.ingest.id } resource "openstack_networking_secgroup_rule_v2" "rtmp" { direction = "ingress" ethertype = "IPv4" protocol = "tcp" port_range_min = 1935 port_range_max = 1935 remote_ip_prefix = var.encoder_cidr security_group_id = openstack_networking_secgroup_v2.ingest.id } resource "openstack_networking_secgroup_rule_v2" "srt" { direction = "ingress" ethertype = "IPv4" protocol = "udp" port_range_min = var.srt_port port_range_max = var.srt_port remote_ip_prefix = var.encoder_cidr security_group_id = openstack_networking_secgroup_v2.ingest.id } resource "openstack_networking_port_v2" "ingest" { name = "${var.instance_name}-port" network_id = openstack_networking_network_v2.private.id security_group_ids = [openstack_networking_secgroup_v2.ingest.id] fixed_ip { subnet_id = openstack_networking_subnet_v2.private.id } depends_on = [openstack_networking_router_interface_v2.private] } resource "openstack_compute_instance_v2" "ingest" { name = var.instance_name flavor_name = var.ingest_flavor key_pair = var.key_name user_data = templatefile("${path.module}/cloud-init/ingest.yaml", { ingest_server = var.ingest_server application_name = var.application_name stream_key = var.stream_key restream_targets_lines = local.restream_targets_lines enable_hls_recording = var.enable_hls_recording ? "true" : "false" s3_endpoint = var.s3_endpoint s3_region = var.s3_region s3_access_key = var.s3_access_key s3_secret_key = var.s3_secret_key hls_bucket = var.enable_hls_recording ? aws_s3_bucket.hls[0].id : "" archive_bucket = aws_s3_bucket.archive.id srt_port = var.srt_port }) block_device { uuid = data.openstack_images_image_v2.os.id source_type = "image" destination_type = "volume" volume_size = 40 boot_index = 0 delete_on_termination = true } network { port = openstack_networking_port_v2.ingest.id } depends_on = [openstack_networking_router_interface_v2.private] } resource "openstack_networking_floatingip_v2" "ingest" { pool = var.external_network } resource "openstack_networking_floatingip_associate_v2" "ingest" { floating_ip = openstack_networking_floatingip_v2.ingest.address port_id = openstack_networking_port_v2.ingest.id }variables.tfHCLvariable "key_name" { description = "Existing SSH keypair name in the project for compute instances" type = string } variable "ingest_flavor" { description = "Compute flavor for the ingest VM (CPU-only; size for concurrent FFmpeg restream jobs)" type = string default = "c2a.large" } variable "image_name" { description = "Boot image name" type = string default = "Ubuntu-24.04" } 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 = "Private subnet CIDR for the ingest instance" type = string default = "192.168.75.0/24" } variable "instance_name" { description = "Ingest instance display name" type = string default = "live-ingest" } variable "ingest_server" { description = "Ingest software to install: nginx-rtmp or srs" type = string default = "nginx-rtmp" validation { condition = contains(["nginx-rtmp", "srs"], var.ingest_server) error_message = "ingest_server must be nginx-rtmp or srs." } } variable "encoder_cidr" { description = "Source CIDR allowed to push RTMP (1935/tcp) and SRT (srt_port/udp). Restrict to your encoder or studio egress IP." type = string default = "0.0.0.0/0" } variable "admin_cidr" { description = "Source CIDR allowed for SSH (22/tcp) administration" type = string default = "0.0.0.0/0" } variable "srt_port" { description = "UDP port opened for SRT ingest when using srs" type = number default = 10080 } variable "application_name" { description = "RTMP application name encoders push to (for example live)" type = string default = "live" } variable "stream_key" { description = "RTMP stream key encoders must publish (empty accepts any key on nginx-rtmp)" type = string default = "" sensitive = true } variable "restream_targets" { description = "Downstream RTMP push URLs (one per line in cloud-init config). Use pass-through copy where possible." type = list(string) default = [] } variable "enable_hls_recording" { description = "When true, record HLS segments locally and upload to Object Storage" type = bool default = false } variable "hls_bucket_name" { description = "Object Storage bucket for HLS replay segments (required when enable_hls_recording is true)" type = string default = "" } variable "archive_bucket_name" { description = "Object Storage bucket for long-term stream recording archive" type = string } variable "s3_access_key" { description = "EC2-compatible access key for Object Storage (required when enable_hls_recording is true)" type = string default = "" sensitive = true } variable "s3_secret_key" { description = "EC2-compatible secret key paired with s3_access_key" type = string default = "" sensitive = true } variable "s3_endpoint" { description = "Quake AI S3-compatible endpoint URL" type = string default = "https://object.us-east-1.rumble.cloud" } variable "s3_region" { description = "S3 region identifier passed to the AWS provider" type = string default = "us-east-1" }outputs.tfHCLoutput "ingest_instance_id" { description = "Nova instance ID for the ingest server" value = openstack_compute_instance_v2.ingest.id } output "ingest_floating_ip" { description = "Public floating IP encoders push RTMP/SRT to" value = openstack_networking_floatingip_v2.ingest.address } output "rtmp_publish_url" { description = "RTMP URL for OBS or hardware encoders (append stream key if configured)" value = "rtmp://${openstack_networking_floatingip_v2.ingest.address}/${var.application_name}" } output "hls_bucket" { description = "Object Storage bucket for HLS replay (empty when recording is disabled)" value = var.enable_hls_recording ? aws_s3_bucket.hls[0].id : "" } output "floating_ip_count" { description = "Floating IPs this template allocates (one public ingest endpoint)" value = 1 }versions.tfHCLterraform { required_version = ">= 1.6.0" required_providers { openstack = { source = "terraform-provider-openstack/openstack" version = "~> 2.0" } aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "openstack" {} provider "aws" { region = var.s3_region access_key = var.s3_access_key secret_key = var.s3_secret_key skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true endpoints { s3 = var.s3_endpoint } }terraform.tfvars.exampleHCL# Required key_name = "YOUR_KEY_NAME" # ingest_server = "nginx-rtmp" # ingest_flavor = "c2a.large" # application_name = "live" # stream_key = "your-secret-stream-key" # encoder_cidr = "203.0.113.10/32" # admin_cidr = "203.0.113.10/32" # restream_targets = [ # "rtmp://a.rtmp.youtube.com/live2/YOUR_YOUTUBE_KEY", # "rtmp://live.twitch.tv/app/YOUR_TWITCH_KEY", # ] # EC2-compatible credentials from: openstack ec2 credentials create # Required for tofu plan even when HLS recording is disabled (AWS provider init). # s3_access_key = "YOUR_EC2_ACCESS_KEY" # s3_secret_key = "YOUR_EC2_SECRET_KEY" # Optional HLS replay to Object Storage # enable_hls_recording = true # hls_bucket_name = "my-live-hls-replay"cloud-init/ingest.yamlYAML#cloud-config package_update: true package_upgrade: true packages: - ffmpeg write_files: - path: /etc/live-ingest/restream-targets.txt permissions: "0644" content: | ${restream_targets_lines} - path: /etc/live-ingest/env permissions: "0600" content: | INGEST_SERVER=${ingest_server} APPLICATION_NAME=${application_name} STREAM_KEY=${stream_key} ENABLE_HLS_RECORDING=${enable_hls_recording} S3_ENDPOINT=${s3_endpoint} S3_REGION=${s3_region} AWS_ACCESS_KEY_ID=${s3_access_key} AWS_SECRET_ACCESS_KEY=${s3_secret_key} HLS_BUCKET=${hls_bucket} ARCHIVE_BUCKET=${archive_bucket} SRT_PORT=${srt_port} runcmd: - | set -eu apt-get install -y python3-pip pip3 install --break-system-packages awscli . /etc/live-ingest/env mkdir -p /var/lib/live-ingest/hls if [ "$INGEST_SERVER" = "srs" ]; then apt-get install -y docker.io systemctl enable --now docker docker run -d --name srs --restart unless-stopped \ -p 1935:1935 -p "$SRT_PORT:$SRT_PORT/udp" \ -v /var/lib/live-ingest/hls:/usr/local/srs/objs/nginx/html \ ossrs/srs:5 else apt-get install -y nginx libnginx-mod-rtmp PUSH_LINES="" while IFS= read -r target; do case "$target" in ""|\#*) continue ;; esac PUSH_LINES="$${PUSH_LINES} push $${target};"$'\n' done < /etc/live-ingest/restream-targets.txt cat > /etc/nginx/nginx.conf <<NGINX load_module modules/ngx_rtmp_module.so; worker_processes auto; events { worker_connections 1024; } rtmp { server { listen 1935; chunk_size 4096; application $${APPLICATION_NAME} { live on; record off; $${PUSH_LINES} } } } http { server { listen 8080; location /hls { root /var/lib/live-ingest; types { application/vnd.apple.mpegurl m3u8; } } } } NGINX systemctl enable nginx systemctl restart nginx fiREADME.mdMarkdown# Live RTMP/SRT ingest and restream CPU ingest VM with one floating IP for encoder pushes, optional downstream RTMP restream targets, and optional HLS segment upload to Object Storage. **Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos. ## Prerequisites - OpenTofu >= 1.6.0 or Terraform >= 1.6.0 - Quake AI account with OpenStack credentials - One available floating IP quota slot - EC2-compatible Object Storage credentials when `enable_hls_recording` is true ## Usage 1. Clone or copy this template directory 2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values 3. Source your OpenStack credentials: `source openrc.sh` 4. Initialize: `tofu init` 5. Preview: `tofu plan` 6. Apply: `tofu apply` ## Variables | Name | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances | | `ingest_flavor` | string | no | `c2a.large` | CPU flavor for the ingest VM | | `ingest_server` | string | no | `nginx-rtmp` | `nginx-rtmp` or `srs` | | `encoder_cidr` | string | no | `0.0.0.0/0` | CIDR allowed to push RTMP/SRT | | `admin_cidr` | string | no | `0.0.0.0/0` | CIDR allowed for SSH | | `restream_targets` | list(string) | no | `[]` | Downstream RTMP push URLs | | `enable_hls_recording` | bool | no | `false` | Upload HLS segments to Object Storage | | `hls_bucket_name` | string | when recording | `""` | Replay bucket name | ## Documentation Full documentation: [Live ingest and restream template](/docs/automation/templates/live-ingest-restream)
Resources, parameters, and variables
key_namerequiredingest_flavor="c2a.large"image_name="Ubuntu-24.04"external_network="PublicStatic"private_cidr="192.168.75.0/24"instance_name="live-ingest"ingest_server="nginx-rtmp" validation {encoder_cidr="0.0.0.0/0"admin_cidr="0.0.0.0/0"srt_port=10080application_name="live"stream_key=""restream_targets=[]enable_hls_recording=falsehls_bucket_name=""s3_access_key=""s3_secret_key=""s3_endpoint="https://object.us-east-1.rumble.cloud"s3_region="us-east-1"
Outputs#
| Output | Description |
|---|---|
ingest_floating_ip | Public IP for encoder pushes |
rtmp_publish_url | Base RTMP URL (rtmp://FIP/live) |
floating_ip_count | Always 1 for this pattern |
hls_bucket | Replay bucket name when recording is enabled |
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
CPU render-farm worker pool
Shares: Cloud Init, Object Storage
Deploy the live RTMP/SRT ingest and restream template with OpenTofu
Shares: Cloud Init, Object Storage
Networks
Prerequisite
MinIO + Apache Iceberg lakehouse
Shares: Object Storage, Security Groups
Deploy the audio post-production worker template with OpenTofu
Shares: Cloud Init, Object Storage