Apache Gravitino is a federated metadata catalog: it presents many different data systems, Hive, Iceberg, relational databases, object storage, files, as one governed namespace, so a single engine or agent can discover and query across all of them, with access control and audit applied at the source rather than bolted onto each tool.
This playground is a complete, runnable environment for seeing that in action. It brings up Hive, HDFS, Trino, Spark, MySQL, PostgreSQL, Ranger, Jupyter, Prometheus, Grafana, and a Gravitino server, all wired together, so you can join data across catalogs, govern it with roles and policies, and let an AI agent reason over the same governed metadata through the Model Context Protocol.
Depending on your network and computer, startup takes 3-5 minutes. Once it is running, open http://localhost:8090 for the Gravitino Web UI. The quickest way to see what the playground does is the Jupyter notebooks.
You need Docker installed and running. Docker Desktop (macOS and Windows) or Docker Engine (Linux) both include Docker Compose, which the playground uses to bring up the services, so there is nothing separate to install for Compose. Get Docker from docs.docker.com/get-docker. Git is optional, needed only if you clone the repository instead of using the one-line installer.
The playground needs roughly 2 CPU cores, 8 GB RAM, and 25 GB of free disk. See System requirements for details.
There are two ways to get the playground. Use one or the other, not both.
Downloads the playground and starts it in a single step:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/apache/gravitino-playground/HEAD/install.sh)"git clone https://github.com/apache/gravitino-playground.git
cd gravitino-playground
./playground.sh start./playground.sh start # full stack, MCP server on by default
./playground.sh start --disable-mcp # bring the stack up without the MCP server
./playground.sh start --enable-ranger # add Ranger authorization
./playground.sh start --enable-auth # enable authentication--enable-auth and --enable-ranger cannot be combined.
Run these from the playground directory, whichever option you used to install it (gravitino-playground for git, gravitino-playground-main for the installer).
./playground.sh statusWhen all containers are healthy, open the Gravitino Web UI at http://localhost:8090.
./playground.sh stopstop halts the containers but keeps them in place, so the next ./playground.sh start resumes them in seconds instead of re-running initialization and health checks. All data is preserved: Gravitino metadata in the data directory of this repo, and MySQL, PostgreSQL, HDFS, and Ranger contents in named Docker volumes.
./playground.sh downdown removes the containers and network (data in named volumes is still kept). Use this when you want a clean rebuild, for example after changing an init script or bumping an image tag. To remove everything including data and start completely fresh:
docker compose -p gravitino-playground down -v
rm -rf dataThe notebooks are the quickest way to see what the playground does, and for many people they are all you need. Open Jupyter at http://localhost:18888, open a notebook, and run the cells top to bottom.
-
gravitino-mcp-demo.ipynbconnects to the Gravitino MCP server, lists the tools Gravitino exposes, seeds governance tags, walks a governed table, and (with an LLM API key) answers plain-language questions by calling the tools itself. See the Gravitino MCP Server section. -
gravitino_llamaIndex_demo.ipynbis a retrieval-augmented generation demo that treats Gravitino as a unified data source. Structured city statistics live in MySQL (a relational catalog) and city descriptions live in PDF files (a fileset catalog); a single natural-language question is answered from both by joining a SQL index over the table with a vector index over the documents. It requires anOPENAI_API_KEYfrom an account with available credit. Set it the same way as the Anthropic key, in a.envfile in the repo root, which Compose passes through to Jupyter (otherwise the notebook prompts for it at runtime):echo 'OPENAI_API_KEY=sk-...' >> .env # do not commit this file
gravitino-trino-example.ipynbdrives Trino against Gravitino-managed catalogs, showing how metadata operations flow through Gravitino rather than a per-engine catalog.gravitino-spark-trino-example.ipynbwrites with Spark and reads the same table with Trino, showing one Gravitino-managed metadata layer serving both engines interchangeably.
gravitino-access-control-example.ipynbdemonstrates Ranger-backed authorization: it authorizes a Hive catalog through Gravitino, then uses Spark to show operations allowed or denied per user, with the results visible in the Ranger admin UI. Requires--enable-ranger(see Ranger Authorization). For Gravitino's own built-in access control, without Ranger, see Access Control under Iceberg REST Service.gravitino-fileset-example.ipynbmanages the fileset metadata lifecycle: creating a fileset catalog and schema, registering managed and external filesets, and observing how each behaves in HDFS.gravitino-gvfs-example.ipynbreads unstructured data (PDFs) through the Gravitino Virtual File System, so file access goes through a governed fileset path rather than a raw storage location. No LLM required.
The playground runs the Gravitino MCP server, which exposes Gravitino metadata to AI agents over the Model Context Protocol. An agent connected here does not get raw files; it gets governed metadata (catalogs, schemas, tables, tags, and policies) through a uniform tool surface, with every call subject to Gravitino's authorization and recorded in its audit log. It is on by default and listens on port 8000; start with --disable-mcp to leave it out. For more information, see the Gravitino MCP server documentation.
The gravitino-mcp-demo.ipynb notebook is the fastest way to see the value. Open Jupyter at http://localhost:18888, open the notebook, and run the cells. It connects to the MCP server, lists the tools Gravitino offers, seeds a couple of tags, walks a governed table, and, if an LLM API key is available, lets you ask questions in plain language that the agent answers by calling the tools itself.
The agentic section never stores a key in the notebook. If ANTHROPIC_API_KEY is set in the environment it is used automatically; otherwise the notebook prompts for one at runtime (masked, kept only in the kernel session), so each person can supply their own. Leaving the prompt blank skips the agentic cells and runs everything else.
To pre-set the key so there is no prompt (handy for your own iteration), put it in a .env file in the repo root, which Compose reads automatically:
echo 'ANTHROPIC_API_KEY=sk-ant-...' >> .env # do not commit this fileThe server speaks streamable HTTP at http://localhost:8000/mcp. Point any MCP-capable client (Claude Desktop, Cursor, a custom agent) at that URL:
{
"mcpServers": {
"gravitino": { "url": "http://localhost:8000/mcp" }
}
}If your client runs on a different machine than the playground, reach the endpoint over an SSH tunnel rather than exposing the port:
ssh -L 8000:localhost:8000 <user>@<playground-host>When authentication is enabled (--enable-auth), the server forwards each request's Authorization header to Gravitino, which authorizes the call per principal. Add the header in your client to make MCP calls run under a specific identity:
{
"mcpServers": {
"gravitino": {
"url": "http://localhost:8000/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}To confirm the server is reachable and browse its tools directly, use the MCP Inspector:
npx @modelcontextprotocol/inspectorOpen the URL it prints, set Transport Type to Streamable HTTP, enter http://localhost:8000/mcp (tunnel first if remote), and click Connect. Under the Tools tab, List Tools shows the full surface, and running get_list_of_catalogs returns the demo catalogs. A 400 Bad Request to a plain browser GET on /mcp is expected: the protocol requires an initialize handshake, so that response confirms the server is up.
Trino queries metadata that Gravitino federates across catalogs. For more information, see the Gravitino Trino connector documentation.
Log in to the Trino container:
docker exec -it playground-trino bashThen open the Trino CLI:
trinoGravitino presents every registered data source as a Trino catalog. Listing them shows Hive, Iceberg, MySQL, and PostgreSQL all reachable from one engine:
SHOW CATALOGS;The point of federation is a single query that spans systems. Here HR data lives in PostgreSQL and sales data in Hive, and Gravitino lets Trino join them directly, to find the employee with the largest sales amount:
SELECT given_name, family_name, job_title, sum(total_amount) AS total_sales
FROM catalog_hive.sales.sales AS s,
catalog_postgres.hr.employees AS e
WHERE s.employee_id = e.employee_id
GROUP BY given_name, family_name, job_title
ORDER BY total_sales DESC
LIMIT 1;Spark reads and writes the same Gravitino-managed metadata as Trino, so the two engines operate over one catalog. For more information, see the Gravitino Spark connector documentation.
Log in to the Spark container:
docker exec -it playground-spark bashThen open the Spark SQL client:
cd /opt/spark && /bin/bash bin/spark-sqlFor a worked example of writing with Spark and reading the same table with Trino, see the gravitino-spark-trino-example.ipynb notebook in the Jupyter Notebooks section.
A common migration scenario: some tables stay in Hive while others move to Iceberg. Gravitino provides an Iceberg REST catalog service for exactly this. Spark writes Iceberg tables through the REST catalog, and Trino reads them alongside Hive tables, because catalog_rest (Spark) and catalog_iceberg (Gravitino and Trino) share the same Iceberg JDBC backend and so see the same data.
The playground wires Spark to the REST service in spark-defaults.conf:
spark.sql.catalog.catalog_rest org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.catalog_rest.type rest
spark.sql.catalog.catalog_rest.uri http://gravitino:9001/iceberg/
In spark-sql, write an Iceberg table through the REST catalog:
USE catalog_rest;
CREATE DATABASE IF NOT EXISTS sales;
CREATE TABLE sales.customers (customer_id int, customer_name varchar(100));
INSERT INTO sales.customers VALUES (11, 'Rory Brown'), (12, 'Jerry Washington');The same table is immediately visible to Trino as catalog_iceberg.sales.customers, because both point at one Iceberg backend through Gravitino. The gravitino-spark-trino-example.ipynb notebook runs this end to end. For more information, see the Iceberg REST catalog service documentation.
Gravitino provides built-in access control for the Iceberg REST server, enforcing catalog, schema, and table level privileges without requiring an external authorization service like Ranger. You manage users, roles, and privileges through the Gravitino API, and the Iceberg REST server enforces them.
In Basic Authentication mode, Gravitino does not verify the password: it trusts the supplied username and makes access control decisions from it, so any client that can reach the REST endpoint can act as any user. That is fine for the playground and nowhere else. Production deployments use real authentication, such as OAuth2 token validation, described in the Gravitino security documentation.
Start the playground with --enable-auth, which turns on Gravitino's access control so the Iceberg REST server enforces privileges.
Spark connects as a given user by passing basic-auth conf; only the username changes between sessions:
bin/spark-sql \
--conf spark.sql.catalog.catalog_rest.rest.auth.type=basic \
--conf spark.sql.catalog.catalog_rest.rest.auth.basic.username=<user> \
--conf spark.sql.catalog.catalog_rest.rest.auth.basic.password=123Create two users through the Gravitino API and make manager the metalake owner:
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" -H "Content-Type: application/json" \
-d '{"name":"manager"}' \
http://localhost:8090/api/metalakes/metalake_demo/users
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" -H "Content-Type: application/json" \
-d '{"name":"data_analyst"}' \
http://localhost:8090/api/metalakes/metalake_demo/users
curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" -H "Content-Type: application/json" \
-d '{"name":"manager","type":"USER"}' \
http://localhost:8090/api/metalakes/metalake_demo/owners/metalake/metalake_demoAs manager in spark-sql, create the table the rest of the demo controls access to:
USE catalog_rest;
CREATE DATABASE IF NOT EXISTS demo_db;
CREATE TABLE demo_db.employees (employee_id int, name string) USING iceberg;As data_analyst, with no privileges yet, the schema is not even visible:
USE catalog_rest.demo_db; -- Fails: no USE_SCHEMA privilegeCreate a role granting USE_CATALOG, USE_SCHEMA, and SELECT_TABLE, and assign it to data_analyst. The role references catalog_iceberg (the catalog's name in Gravitino), which is the same catalog Spark reaches as catalog_rest:
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" -H "Content-Type: application/json" \
-u manager:123 \
-d '{
"name": "analyst_role",
"securableObjects": [
{"fullName": "catalog_iceberg", "type": "CATALOG",
"privileges": [{"name": "USE_CATALOG", "condition": "ALLOW"}]},
{"fullName": "catalog_iceberg.demo_db", "type": "SCHEMA",
"privileges": [{"name": "USE_SCHEMA", "condition": "ALLOW"}]},
{"fullName": "catalog_iceberg.demo_db.employees", "type": "TABLE",
"privileges": [{"name": "SELECT_TABLE", "condition": "ALLOW"}]}
]
}' \
http://localhost:8090/api/metalakes/metalake_demo/roles
curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" -H "Content-Type: application/json" \
-u manager:123 \
-d '{"roleNames": ["analyst_role"]}' \
http://localhost:8090/api/metalakes/metalake_demo/permissions/users/data_analyst/grantNow the same query as data_analyst succeeds. Access was denied before the grant and allowed after, enforced by Gravitino at the REST catalog. See the access control documentation for details.
The playground seeds catalog_iceberg.analytics.orders at startup, partitioned by region and written in two commits, so it has snapshot history immediately. List the snapshots, then read the table as of an earlier commit:
SELECT * FROM catalog_iceberg."analytics"."orders$snapshots";
SELECT COUNT(*) FROM catalog_iceberg.analytics.orders FOR VERSION AS OF <snapshot_id>;Gravitino manages Hive tables through the Hive Metastore (HMS), presenting them as a catalog alongside Iceberg, MySQL, and PostgreSQL. Metadata operations on the Hive catalog go through Gravitino to the HMS, so Hive data is governed and queried the same way as any other source. For more information, see the Apache Hive catalog documentation.
Gravitino provides access control for Hive tables using the Ranger plugin. For example, a company has a manager and several staff members: the manager creates a Hive catalog, defines roles, and assigns those roles to staff.
Start the playground with --enable-ranger, then open the gravitino-access-control-example.ipynb notebook, which authorizes a Hive catalog through Gravitino and uses Spark to show operations being allowed or denied per user. You can log in to the Ranger admin UI at http://localhost:6080 (user admin) to see the permissions. For more information, see the access control documentation.
Gravitino can automate governance from metadata alone. This example marks a table as stale with a statistic, defines a policy for what "stale" means, and runs a job that drops tables the policy flags, all through the Gravitino API, with no engine involved. The three build on each other: statistics record facts about a table, a policy interprets them, and a job acts.
All calls below go to the Gravitino API at http://localhost:8090. They use the seeded catalog_hive.sales.customers table.
Mark the table as last accessed 100 days ago, past the 90-day threshold the policy will use:
OLD_DATE=$(date -u -d '100 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-100d +%Y-%m-%dT%H:%M:%SZ)
curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-d "{\"updates\": {\"custom-lastAccessTime\": \"$OLD_DATE\"}}" \
http://localhost:8090/api/metalakes/metalake_demo/objects/table/catalog_hive.sales.customers/statisticsDefine a custom policy that flags tables not accessed for more than 90 days:
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-d '{
"name": "unused_table_policy",
"comment": "Flags tables not accessed for 90+ days",
"policyType": "custom",
"enabled": true,
"content": {
"supportedObjectTypes": ["TABLE"],
"properties": {
"checkStatistic": "custom-lastAccessTime",
"threshold": "90d"
}
}
}' \
http://localhost:8090/api/metalakes/metalake_demo/policiescurl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-d '{"policiesToAdd": ["unused_table_policy"]}' \
http://localhost:8090/api/metalakes/metalake_demo/objects/table/catalog_hive.sales.customers/policiesRegister a shell job template that takes a table and drops it if the policy's statistic shows it is stale. The script itself is a short shell file (it reads the statistic, compares against the threshold, and calls the Gravitino delete API); register it as a template, then run it against the table:
# Register the template (executable is a shell script placed in the Gravitino container)
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-d '{
"jobTemplate": {
"name": "drop_unused_table_job",
"jobType": "shell",
"executable": "file:///tmp/drop_unused_tables.sh",
"arguments": ["{{catalog}}", "{{schema}}", "{{table}}"]
}
}' \
http://localhost:8090/api/metalakes/metalake_demo/jobs/templates
# Run it against the stale table
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-d '{
"jobTemplateName": "drop_unused_table_job",
"jobConf": {"catalog": "catalog_hive", "schema": "sales", "table": "customers"}
}' \
http://localhost:8090/api/metalakes/metalake_demo/jobs/runsThe run returns a jobRunId. Query the run to see its status (QUEUED, RUNNING, SUCCEEDED, FAILED) and output:
curl -X GET -H "Accept: application/vnd.gravitino.v1+json" \
http://localhost:8090/api/metalakes/metalake_demo/jobs/runs/<jobRunId>Once it succeeds, the table is gone. For the full job script and API details, see Manage jobs, Manage policies, and Manage statistics.
Open Grafana at http://localhost:13000. In the navigation menu, click Dashboards -> Gravitino Playground, then experiment with the default template.
2 CPU cores, 8 GB RAM, 25 GB disk storage, macOS or Linux (verified on Ubuntu 22.04, Ubuntu 24.04, and Amazon Linux).
The playground runs several services. The TCP ports used may clash with existing services you run, such as MySQL or Postgres.
| Docker container | Ports used |
|---|---|
| playground-gravitino | 8090 9001 |
| playground-hive | 3307 19000 19083 60070 |
| playground-ranger | 6080 |
| playground-mysql | 13306 |
| playground-spark | 14040 |
| playground-postgresql | 15432 |
| playground-trino | 18080 |
| playground-jupyter | 18888 |
| playground-prometheus | 19090 |
| playground-grafana | 13000 |
| playground-gravitino-mcp | 8000 |
The playground is preconfigured for local evaluation and is not a production reference architecture. The defaults reflect that:
| Aspect | Configuration | Notes |
|---|---|---|
| Authentication | None | Gravitino trusts the username presented by clients. See the note on Basic Authentication in the access control section below. |
| Authorization | Disabled | Enable Gravitino native access control with --enable-auth, or Ranger enforcement for Hive with --enable-ranger. |
| Transport | Plain HTTP | No TLS on any service, including Trino and the Gravitino API. |
| Gravitino metadata store | Embedded H2 in the data directory |
Only the Gravitino server accesses this store, so an embedded database suffices. Wiped by the full reset. |
| Iceberg catalog backend | JDBC, MySQL db database |
Shared by catalog_iceberg (Gravitino, Trino) and catalog_rest (Spark, through the Iceberg REST service), so it needs a database all three can reach. |
| Table storage | HDFS in the hive container |
hdfs://hive:9000 for both Hive and Iceberg warehouses. No object storage is involved. |
| Credentials | Hardcoded demo values | For example, MySQL uses mysql/mysql. |
The playground has no authentication or TLS, so any reachable port grants full access to that service. If you run the playground on a remote host, control who can reach the ports, for example with firewall rules scoped to your address or an SSH tunnel.
The playground stores state in Docker volumes and in the data directory of this repo. See the Stop section above for how to reset the playground completely.
Apache®, Apache Gravitino™, Apache Hive™, Apache Iceberg™, and Apache Spark™ are either registered trademarks or trademarks of the Apache Software Foundation in the United States and/or other countries.