Ap
Databases AWS

Apache Pinot Real-Time OLAP Datastore on AWS User Guide

| Product: Apache Pinot on AWS

Overview

This image runs Apache Pinot, a real-time distributed OLAP datastore designed for low-latency, high-concurrency analytics over large event and time-series datasets. It is deployed as a single-instance cluster: the bundled Apache ZooKeeper, the Pinot Controller (which hosts the web console), the Pinot Broker (the query endpoint) and the Pinot Server each run as their own systemd service, started in dependency order on boot and restarted on failure.

Pinot is installed under /opt/pinot and runs as a dedicated unprivileged pinot system account on a headless OpenJDK 17 runtime. Its persistent cluster state - the ZooKeeper data, the Controller data directory and segment store, and the Server's segment storage - lives on a dedicated, independently resizable EBS data volume mounted by filesystem UUID at /var/lib/pinot, so your ingested data and cluster metadata survive instance replacement and are captured into the image layout.

The Pinot web console is served on port 80 by an nginx reverse proxy. The Pinot Controller console and the Broker query endpoint have no native authentication, so this image keeps every Pinot component and ZooKeeper off the public network - the security group opens only port 22 (SSH) and port 80 (the console) - and fronts the console with an authenticated nginx reverse proxy. The console password is generated on the first boot of every deployed instance, so two instances launched from the same AMI never share a password. It is written to /root/pinot-credentials.txt with mode 0600 so that only the root user can read it.

Apache Pinot Cluster Manager

Prerequisites

Before you deploy this image you need:

  • An Amazon Web Services account where you can launch EC2 instances
  • IAM permissions to launch instances, create security groups, and subscribe to AWS Marketplace products
  • An EC2 key pair in the target Region for SSH access to the instance
  • A VPC and subnet in the target Region, with a security group allowing inbound port 22 from your management network and port 80 for the web console
  • The AWS CLI (version 2) installed locally if you plan to deploy from the command line

Step 1: Launch the Instance from the AWS Marketplace

Sign in to the AWS Management Console, open the EC2 service, and select Launch instance. Under Application and OS Images choose AWS Marketplace AMIs and search for Apache Pinot. Select the cloudimg listing and choose Select, then Continue on the subscription summary.

Pick an instance type of m5.large or larger (Pinot runs several JVMs; size it for your data volume and query concurrency). Choose your EC2 key pair under Key pair (login). Under Network settings select your VPC and subnet, and either create or select a security group that opens port 22 from your management network and port 80 for the web console. Leave the root volume at the default size or larger, and keep the attached data volume that holds the Pinot cluster state.

Select Launch instance. First-boot initialisation runs immediately, and the Pinot cluster then takes about a minute to come up in dependency order after the instance state becomes Running and the status checks pass.

Step 2: Launch the Instance from the AWS CLI

The following block launches an instance from the cloudimg Apache Pinot Marketplace AMI into an existing subnet and security group. Replace <ami-id> with the AMI ID shown on the Marketplace listing, <key-name> with your EC2 key pair name, <subnet-id> with your subnet ID, and <security-group-id> with a security group that opens ports 22 and 80 as described above.

aws ec2 run-instances \
  --image-id <ami-id> \
  --instance-type m5.large \
  --key-name <key-name> \
  --subnet-id <subnet-id> \
  --security-group-ids <security-group-id> \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=apache-pinot}]'

When the instance reaches the Running state and its status checks pass, note its public IP address or DNS name from the EC2 console or with aws ec2 describe-instances.

Step 3: Connect to Your Instance

Connect over SSH using your key pair and the login user for your operating system variant.

OS variant SSH login user
Ubuntu 24.04 ubuntu
ssh -i /path/to/your-key.pem ubuntu@<public-ip>

Step 4: Retrieve the Console Password

The Pinot console password is unique to your instance and was generated on first boot. Read it as root:

sudo cat /root/pinot-credentials.txt

The file lists the console URL, the administrator user (admin) and the generated password. It is mode 0600 and owned by root, so only the root user can read it. Every command below that needs the password reads it straight from this file into a shell variable, so you never have to copy or paste it.

Step 5: Confirm the Services Are Running

The Pinot cluster and nginx run as systemd services. Confirm they are all active:

systemctl is-active pinot-zookeeper.service pinot-controller.service pinot-broker.service pinot-server.service nginx.service

nginx serves an unauthenticated health endpoint on port 80 that is ideal for load-balancer health checks. Confirm it answers:

curl -fsS http://127.0.0.1/health

Step 6: Verify the Authenticated Console

The console itself requires the per-instance password. First confirm that an unauthenticated request is rejected:

curl -s -o /dev/null -w 'unauthenticated console: HTTP %{http_code}\n' http://127.0.0.1/

You should see HTTP 401. Now read the password from the credentials file into a shell variable (it is never printed) and confirm the authenticated console returns HTTP 200:

PW=$(sudo awk -F= '/^PINOT_ADMIN_PASSWORD=/{print $2}' /root/pinot-credentials.txt)
curl -sL -o /dev/null -w 'authenticated console: HTTP %{http_code}\n' -u "admin:$PW" http://127.0.0.1/

Step 7: Open the Web Console

Browse to http://<public-ip>/ and sign in as user admin with the password from Step 4. The console opens on the Cluster Manager, which shows the live Controller, Broker and Server instances and the tenant and table counts.

The Query Console gives you an interactive SQL editor. Pick a table on the left, run an aggregation over your data and get ranked results in milliseconds, with per-query response statistics shown above the result grid.

Pinot SQL Query Console with results

Click a table name to open its detail view, which shows the table configuration, the schema (dimension, metric and date-time columns) and the segments loaded on each Server instance.

Pinot table schema and segments

Step 8: Inspect the Cluster and Tables from the Command Line

The Pinot Controller REST API is reachable through the authenticated proxy. List the tables in the cluster, reading the password from the credentials file so it is never printed:

PW=$(sudo awk -F= '/^PINOT_ADMIN_PASSWORD=/{print $2}' /root/pinot-credentials.txt)
curl -s -u "admin:$PW" http://127.0.0.1/tables

Confirm the Controller reports itself healthy through the same authenticated proxy:

PW=$(sudo awk -F= '/^PINOT_ADMIN_PASSWORD=/{print $2}' /root/pinot-credentials.txt)
curl -s -u "admin:$PW" http://127.0.0.1/health

Step 9: Create a Table and Ingest Data

Pinot ingests data into a table that is described by a schema. The following worked example creates a small offline table, batch-ingests five rows and queries them. Run it on the instance (it reads the console password from the credentials file). This uses the Controller REST API for schema and table creation, the bundled pinot-admin.sh batch ingestion job for loading data, and the Broker query endpoint for the query.

PW=$(sudo awk -F= '/^PINOT_ADMIN_PASSWORD=/{print $2}' /root/pinot-credentials.txt)
sudo install -d -o pinot -g pinot /var/lib/pinot/example/rawdata /var/lib/pinot/example/segments

# 1. Define the schema (two dimensions, one metric, one time column).
cat <<'JSON' | sudo -u pinot tee /var/lib/pinot/example/schema.json >/dev/null
{"schemaName":"events",
 "dimensionFieldSpecs":[{"name":"id","dataType":"INT"},{"name":"city","dataType":"STRING"}],
 "metricFieldSpecs":[{"name":"amount","dataType":"LONG"}],
 "dateTimeFieldSpecs":[{"name":"ts","dataType":"LONG","format":"1:MILLISECONDS:EPOCH","granularity":"1:MILLISECONDS"}]}
JSON

# 2. Define the offline table.
cat <<'JSON' | sudo -u pinot tee /var/lib/pinot/example/table.json >/dev/null
{"tableName":"events","tableType":"OFFLINE",
 "segmentsConfig":{"timeColumnName":"ts","timeType":"MILLISECONDS","replication":"1"},
 "tenants":{},"tableIndexConfig":{"loadMode":"MMAP"},"metadata":{}}
JSON

curl -s -u "admin:$PW" -X POST -H 'Content-Type: application/json' \
  --data-binary @/var/lib/pinot/example/schema.json http://<public-ip>/schemas
curl -s -u "admin:$PW" -X POST -H 'Content-Type: application/json' \
  --data-binary @/var/lib/pinot/example/table.json  http://<public-ip>/tables

# 3. Provide the data and a batch ingestion job spec.
cat <<'CSV' | sudo -u pinot tee /var/lib/pinot/example/rawdata/data.csv >/dev/null
id,city,amount,ts
1,london,10,1700000000000
2,paris,20,1700000001000
3,london,30,1700000002000
CSV

cat <<'YAML' | sudo -u pinot tee /var/lib/pinot/example/job.yaml >/dev/null
executionFrameworkSpec: {name: standalone,
  segmentGenerationJobRunnerClassName: org.apache.pinot.plugin.ingestion.batch.standalone.SegmentGenerationJobRunner,
  segmentTarPushJobRunnerClassName: org.apache.pinot.plugin.ingestion.batch.standalone.SegmentTarPushJobRunner}
jobType: SegmentCreationAndTarPush
inputDirURI: /var/lib/pinot/example/rawdata
includeFileNamePattern: 'glob:**/*.csv'
outputDirURI: /var/lib/pinot/example/segments
overwriteOutput: true
pinotFSSpecs: [{scheme: file, className: org.apache.pinot.spi.filesystem.LocalPinotFS}]
recordReaderSpec: {dataFormat: csv,
  className: org.apache.pinot.plugin.inputformat.csv.CSVRecordReader,
  configClassName: org.apache.pinot.plugin.inputformat.csv.CSVRecordReaderConfig}
tableSpec: {tableName: events}
pinotClusterSpecs: [{controllerURI: 'http://localhost:9000'}]
pushJobSpec: {pushParallelism: 1, pushAttempts: 2, pushRetryIntervalMillis: 1000}
YAML

# 4. Run the batch ingestion job.
sudo -u pinot env JAVA_OPTS='-Xms256M -Xmx1G' /opt/pinot/current/bin/pinot-admin.sh \
  LaunchDataIngestionJob -jobSpecFile /var/lib/pinot/example/job.yaml

# 5. Query the data through the Broker (a few seconds after ingestion).
curl -s -u "admin:$PW" -H 'Content-Type: application/json' \
  -XPOST http://<public-ip>/sql -d '{"sql":"SELECT city, SUM(amount) FROM events GROUP BY city"}'

You can also use the console's Query Console to run SQL interactively, and drive schema and table creation from your own applications through the same Controller REST API.

Data Persistence and the Dedicated Data Volume

All of Pinot's persistent state lives on a dedicated EBS data volume mounted at /var/lib/pinot: the bundled ZooKeeper data, the Controller data directory and segment store, and the Server's segment storage. The volume is mounted by filesystem UUID via /etc/fstab, so it re-attaches reliably across reboots and instance replacement, and it is captured into the AMI so every launched instance gets the same layout. Because the tier is a separate volume, you can resize it independently for capacity and IOPS, and snapshot it on its own schedule.

findmnt /var/lib/pinot

Security Model

The Pinot Controller console and the Broker query endpoint ship with no native authentication. This image is hardened so that nothing Pinot-related is reachable off the instance except the authenticated console:

  • Network isolation. The security group opens only port 22 (SSH) and port 80 (the console). The ZooKeeper port (2181), the Controller (9000), the Broker (8099) and the Server (8098) are never opened to the network, so they are unreachable from off the instance.
  • Authenticated reverse proxy. nginx is the single public application entry point on port 80. It enforces HTTP basic authentication in front of the Controller console; unauthenticated requests, wrong passwords and the build-time placeholder are all rejected with HTTP 401.
  • Per-instance credentials. The console password is generated uniquely on each instance's first boot and stored in a root-only file. No shared or default console credential ships in the image.

For production, restrict port 80 in your security group to your own CIDR ranges, and terminate TLS at an Application Load Balancer or a reverse proxy in front of the instance so console traffic is encrypted in transit. Deploy the instance in a private subnet where possible.

Maintenance

  • Service control: manage the cluster with sudo systemctl restart pinot-controller.service (and the pinot-zookeeper, pinot-broker, pinot-server and nginx units). The services start in dependency order on boot.
  • Logs: inspect a component with sudo journalctl -u pinot-controller.service -n 100 --no-pager; Pinot's own logs are under /var/lib/pinot/logs.
  • OS updates: apply security updates with sudo apt-get update && sudo apt-get upgrade and reboot during a maintenance window.

Support

cloudimg provides 24/7 technical support for this image by email and live chat. Contact support@cloudimg.co.uk for deployment and instance sizing guidance, schema and table design, ingestion configuration, query tuning, and console authentication and TLS setup, or for any issues including requesting refunds.