Skip to content

Trino federated query engine

Template · Updated Jul 2026
Validated Jul 2026

Trino federated query engine

This validated OpenTofu template composes Compute, Network, and Block Storage into a self-hosted federated SQL query engine you run on infrastructure you control.

What this template does#

Provisions a single instance running Trino, an open-source federated SQL query engine (a self-hosted alternative to Amazon Athena or Starburst):

  • Multi-container Docker stack: one coordinator, one or more workers (default one), and catalog configs for Postgres, ClickHouse, and Iceberg
  • Default sizing: m2a.xlarge (4 vCPU / 16 GiB RAM) and an 80 GiB data volume at /var/lib/docker for container layers and query spill
  • Private network, security group, floating IP; UI restricted to ui_allowed_cidr by default
  • Built-in tpch and tpcds sample catalogs for smoke tests after first boot

Trino federates SQL across the data sources you wire in. Point postgres_host, clickhouse_host, and the Iceberg REST/S3 settings at your warehouse, ClickHouse, and lakehouse stacks, then add credentials in the catalog files on the instance. No credential ships with this template.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist)No default
flavor_nameInstance size (coordinator plus one worker on 4 vCPU / 16 GiB)m2a.xlarge
image_nameOperating system imageUbuntu-24.04
app_nameDisplay name prefix for resourcestrino
volume_sizeBlock volume size in GiB, mounted at /var/lib/docker80
external_networkExternal network for floating IP allocationPublicStatic
private_cidrCIDR for the private subnet10.50.0.0/24
ui_allowed_cidrCIDR allowed to reach the Trino UI on port 808010.50.0.0/24
worker_countTrino worker containers (1 to 3 on this single VM)1
postgres_hostPostgreSQL host for the postgres catalog""
clickhouse_hostClickHouse host for the clickhouse catalog""
iceberg_rest_uriIceberg REST catalog URI""
s3_endpointS3-compatible endpoint for Iceberg""
postgres_portPostgreSQL port for the postgres catalog5432
postgres_databasePostgreSQL database for the postgres cataloganalytics
postgres_userPostgreSQL user for the postgres cataloganalytics
clickhouse_portClickHouse HTTP port for the clickhouse catalog8123
iceberg_warehouseIceberg warehouse URI for the iceberg catalogs3://lake/warehouse
s3_regionS3 region identifier for Iceberg object storageus-east-1

UI access and security#

The Trino web UI listens on port 8080 over plain HTTP. The security group restricts 8080 to ui_allowed_cidr, which defaults to the private network only. Reach the UI over an SSH tunnel, through a reverse proxy on 443, or by setting ui_allowed_cidr to YOUR_IP/32.

Federated catalogs#

cloud-init writes catalog property files under /opt/trino/catalog/ on the instance. Built-in tpch and tpcds catalogs always load. Postgres, ClickHouse, and Iceberg catalogs are created when you set the matching tfvars. Add passwords and object-storage keys on the instance after apply, then run docker compose restart in /opt/trino.

When to use this pattern#

Run a federated SQL query engine over Postgres warehouses, ClickHouse, and Iceberg lake tables on a VM you operate. Trino suits ad hoc analytics, cross-source joins, and the query layer in an open lakehouse stack.

For the warehouse itself, see self-managed PostgreSQL. For a column store, see ClickHouse when that template is on your branch.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on dedicated vCPU.

Starting template$137.40/mo

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

Trino

m2a.xlarge · 4 dedicated vCPU, 16 GiB RAM, 1 Gbps

$132.00/mo

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

m2a.xlarge

4 dedicated vCPU, 16 GiB RAM, 1 Gbps

$132.00

Compute + RAM rate basis

4 vCPU + 16 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Block storage (130 GiB)

130 GiB at $0.08/GiB/mo

$10.40

Public IP (included)

1 included with the custom package

$0.00

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

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 more
$0.00

Private 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.

$0.00

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.

$0.00

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.

$38.40/mo

Production on dedicated CPU

The headline estimate above; predictable steady-load performance.

$137.40/mo

Saves $99.00/mo while you build on shared CPU.

Shared flavors carry less RAM (m2a.xlarge (16 GiB RAM) -> s1a.medium (4 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#

6 files. Download the zip or expand to copy any file.Download trino.zip
Show source (6 files)
main.tfHCL
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" "trino" {
  name        = "${var.app_name}-sg"
  description = "SSH and HTTP/HTTPS for a reverse proxy; editor port 8080 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.trino.id
}

# 80 and 443 carry the editor when it is served over a domain with automatic
# TLS through a reverse proxy (Caddy or Nginx). They are not used until you put
# a proxy in front of trino; see the reference page.
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.trino.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.trino.id
}

# Raw editor HTTP on 8080 is restricted to ui_allowed_cidr (the private
# network by default). Prefer a domain with TLS on 443 for routine access.
# trino's own user management still gates the editor with an owner account set on
# first visit, but the port stays off the public internet by default.
resource "openstack_networking_secgroup_rule_v2" "editor" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 8080
  port_range_max    = 8080
  remote_ip_prefix  = var.ui_allowed_cidr
  security_group_id = openstack_networking_secgroup_v2.trino.id
}

resource "openstack_networking_port_v2" "trino" {
  name               = "${var.app_name}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.trino.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" "trino" {
  name        = var.app_name
  flavor_name = var.flavor_name
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/trino.yaml.tftpl", {
    app_name          = var.app_name
    worker_count      = var.worker_count
    postgres_host     = var.postgres_host
    postgres_port     = var.postgres_port
    postgres_database = var.postgres_database
    postgres_user     = var.postgres_user
    clickhouse_host   = var.clickhouse_host
    clickhouse_port   = var.clickhouse_port
    iceberg_rest_uri  = var.iceberg_rest_uri
    iceberg_warehouse = var.iceberg_warehouse
    s3_endpoint       = var.s3_endpoint
    s3_region         = var.s3_region
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.os.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 50
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.trino.id
  }
}

resource "openstack_compute_volume_attach_v2" "data" {
  instance_id = openstack_compute_instance_v2.trino.id
  volume_id   = openstack_blockstorage_volume_v3.data.id
}

resource "openstack_networking_floatingip_v2" "trino" {
  pool = var.external_network
}

resource "openstack_networking_floatingip_associate_v2" "trino" {
  floating_ip = openstack_networking_floatingip_v2.trino.address
  port_id     = openstack_networking_port_v2.trino.id
}
variables.tfHCL
variable "key_name" {
  description = "SSH keypair name (must already exist in your project)"
  type        = string
}

variable "flavor_name" {
  description = "Instance size. Trino coordinator plus one worker runs on 4 vCPU and 16 GiB RAM."
  type        = string
  default     = "m2a.xlarge"
}

variable "image_name" {
  type    = string
  default = "Ubuntu-24.04"
}

variable "app_name" {
  type    = string
  default = "trino"
}

variable "volume_size" {
  type    = number
  default = 80
}

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" {
  type    = string
  default = "10.50.0.0/24"
}

variable "ui_allowed_cidr" {
  description = "CIDR allowed to reach the Trino web UI on port 8080."
  type        = string
  default     = "10.50.0.0/24"
}

variable "worker_count" {
  type    = number
  default = 1

  validation {
    condition     = var.worker_count >= 1 && var.worker_count <= 3
    error_message = "worker_count must be between 1 and 3."
  }
}

variable "postgres_host" {
  type    = string
  default = ""
}

variable "postgres_port" {
  type    = number
  default = 5432
}

variable "postgres_database" {
  type    = string
  default = "analytics"
}

variable "postgres_user" {
  type    = string
  default = "analytics"
}

variable "clickhouse_host" {
  type    = string
  default = ""
}

variable "clickhouse_port" {
  type    = number
  default = 8123
}

variable "iceberg_rest_uri" {
  type    = string
  default = ""
}

variable "iceberg_warehouse" {
  type    = string
  default = "s3://lake/warehouse"
}

variable "s3_endpoint" {
  type    = string
  default = ""
}

variable "s3_region" {
  type    = string
  default = "us-east-1"
}
outputs.tfHCL
output "instance_id" { value = openstack_compute_instance_v2.trino.id }
output "floating_ip" { value = openstack_networking_floatingip_v2.trino.address }
output "private_ip" { value = openstack_compute_instance_v2.trino.access_ip_v4 }
output "ui_url" { value = "http://${openstack_networking_floatingip_v2.trino.address}:8080" }
output "worker_count" { value = var.worker_count }
versions.tfHCL
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 2.0"
    }
  }
}

provider "openstack" {}
terraform.tfvars.exampleHCL
key_name = "YOUR_KEY_NAME"
cloud-init/trino.yaml.tftpl
#cloud-config
package_update: true
packages: [ca-certificates, curl]
write_files:
  - path: /opt/trino/docker-compose.yml
    content: |
      services:
        trino-coordinator:
          image: trinodb/trino:463
          hostname: trino-coordinator
          restart: unless-stopped
          ports: ["8080:8080"]
          volumes:
            - /opt/trino/coordinator/etc:/etc/trino
            - /opt/trino/catalog:/etc/trino/catalog
            - trino-data:/data/trino
          networks: [trino-net]
        trino-worker-1:
          image: trinodb/trino:463
          hostname: trino-worker-1
          restart: unless-stopped
          depends_on: [trino-coordinator]
          volumes:
            - /opt/trino/worker-1/etc:/etc/trino
            - /opt/trino/catalog:/etc/trino/catalog
            - trino-data:/data/trino
          networks: [trino-net]
      volumes: {trino-data: {}}
      networks: {trino-net: {driver: bridge}}
  - path: /opt/trino/coordinator/etc/config.properties
    content: |
      coordinator=true
      node-scheduler.include-coordinator=false
      http-server.http.port=8080
      discovery.uri=http://trino-coordinator:8080
  - path: /opt/trino/coordinator/etc/node.properties
    content: |
      node.environment=${app_name}
      node.id=coordinator
      node.data-dir=/data/trino
  - path: /opt/trino/coordinator/etc/jvm.config
    content: |
      -server
      -Xmx8G
      -Xms8G
  - path: /opt/trino/worker-1/etc/config.properties
    content: |
      coordinator=false
      http-server.http.port=8080
      discovery.uri=http://trino-coordinator:8080
  - path: /opt/trino/worker-1/etc/node.properties
    content: |
      node.environment=${app_name}
      node.id=worker-1
      node.data-dir=/data/trino
  - path: /opt/trino/worker-1/etc/jvm.config
    content: |
      -server
      -Xmx6G
      -Xms6G
  - path: /opt/trino/catalog/tpch.properties
    content: |
      connector.name=tpch
      tpch.splits-per-node=4
%{ if postgres_host != "" ~}
  - path: /opt/trino/catalog/postgres.properties
    content: |
      connector.name=postgresql
      connection-url=jdbc:postgresql://${postgres_host}:${postgres_port}/${postgres_database}
      connection-user=${postgres_user}
%{ endif ~}
%{ if clickhouse_host != "" ~}
  - path: /opt/trino/catalog/clickhouse.properties
    content: |
      connector.name=clickhouse
      connection-url=jdbc:clickhouse://${clickhouse_host}:${clickhouse_port}/default
      connection-user=default
%{ endif ~}
%{ if iceberg_rest_uri != "" && s3_endpoint != "" ~}
  - path: /opt/trino/catalog/iceberg.properties
    content: |
      connector.name=iceberg
      iceberg.catalog.type=rest
      iceberg.rest-catalog.uri=${iceberg_rest_uri}
      iceberg.rest-catalog.warehouse=${iceberg_warehouse}
      fs.native-s3.enabled=true
      s3.endpoint=${s3_endpoint}
      s3.region=${s3_region}
      s3.path-style-access=true
%{ endif ~}
runcmd:
  - |
    set -e
    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 trinodata "$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
    cd /opt/trino && docker compose up -d
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • key_namerequired
  • flavor_name="m2a.xlarge"
  • image_name="Ubuntu-24.04"
  • app_name="trino"
  • volume_size=80
  • external_network="PublicStatic"
  • private_cidr="10.50.0.0/24"
  • ui_allowed_cidr="10.50.0.0/24"
  • worker_count=1 validation {
  • postgres_host=""
  • postgres_port=5432
  • postgres_database="analytics"
  • postgres_user="analytics"
  • clickhouse_host=""
  • clickhouse_port=8123
  • iceberg_rest_uri=""
  • iceberg_warehouse="s3://lake/warehouse"
  • s3_endpoint=""
  • s3_region="us-east-1"

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

Was this page helpful?