Skip to content

Quickstart on AWS (Glue + S3)

Run Reble against your AWS account: Glue as the catalog, S3 as the warehouse. Nothing to deploy — the CLI is the client. This walkthrough takes about ten minutes from a clean machine and cleans up at the end.

Python 3.14 is not yet tested. Check with python3 --version. If you’re on 3.14, create a 3.13 venv:

Terminal window
python3.13 -m venv reble-test && source reble-test/bin/activate
Terminal window
pip install 'reble[aws]'
reble --version

If the version looks older than the one on PyPI, you have a cached package: pip install --force-reinstall 'reble[aws]'.

Verify credentials and region are configured:

Terminal window
aws configure list # should show an access key and a region
aws sts get-caller-identity # should return your account ID

If not, set them up:

Terminal window
aws configure set aws_access_key_id YOUR_KEY --profile orchestra
aws configure set aws_secret_access_key YOUR_SECRET --profile orchestra
aws configure set region us-east-1 --profile orchestra

Export for this session:

Terminal window
export AWS_PROFILE=orchestra
export AWS_DEFAULT_REGION=us-east-1

If you don’t have one, create it:

Terminal window
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws s3api create-bucket --bucket "reble-test-${ACCOUNT_ID}" --region us-east-1
echo "bucket: reble-test-${ACCOUNT_ID}"

Note the bucket name — you’ll put it in the config below.

Terminal window
mkdir my-warehouse && cd my-warehouse && mkdir models

Create two SQL files — a model is one SQL file that produces one table, and the file name is the table name:

Terminal window
cat > models/stg_orders.sql <<'EOF'
-- kind: table
-- key: order_id
-- Upstream input (not a model): raw_events, where ingestion lands events
-- as they arrive.
with latest as (
select
*,
row_number() over (partition by order_id order by event_ts desc) as _rn
from raw_events
),
typed as (
select
cast(order_id as bigint) as order_id,
cast(user_id as bigint) as user_id,
lower(status) as status,
cast(amount as decimal(12, 2)) as amount,
cast(event_ts as timestamp) as event_ts
from latest
where _rn = 1
)
select *
from typed
where status = 'paid'
and amount > 10
EOF
cat > models/mart_orders.sql <<'EOF'
-- kind: table
-- key: order_id
select
order_id,
user_id,
amount,
round(amount * 0.0825, 2) as tax_amount,
round(amount + amount * 0.0825, 2) as total_with_tax,
date_trunc('day', event_ts) as order_date
from stg_orders
EOF

Now create reble.ymlreplace reble-test-YOUR_ACCOUNT_ID with your bucket name:

reble.yml
version: 1
warehouse:
catalog:
type: glue
region: us-east-1
warehouse: s3://reble-test-YOUR_ACCOUNT_ID/reble
namespace: analytics_test
default_base: main
branching:
git_sync: false # standalone: this project has no git repo
lineage:
models_path: models

Your models read from raw_events — that’s the upstream input your ingestion normally lands. Create a file named seed.py in the project folder:

seed.py
import datetime as dt
import pyarrow as pa
import yaml
from pyiceberg.catalog import load_catalog
cfg = yaml.safe_load(open("reble.yml"))
cat_cfg = dict(cfg["warehouse"]["catalog"])
# reble translates 'region' to 'glue.region' for pyiceberg (0.5.1+)
if "region" in cat_cfg:
cat_cfg["glue.region"] = cat_cfg.pop("region")
cat = load_catalog("reble", **cat_cfg)
ns = cfg["warehouse"]["namespace"]
cat.create_namespace_if_not_exists(ns)
cat.create_table(
f"{ns}.raw_events",
schema=pa.schema([
("order_id", pa.int64()),
("user_id", pa.int64()),
("status", pa.string()),
("amount", pa.float64()),
("event_ts", pa.timestamp("us")),
]),
).append(pa.table({
"order_id": [1, 2, 3, 4, 5, 6],
"user_id": [101, 102, 103, 104, 105, 106],
"status": ["paid", "paid", "paid", "pending", "paid", "paid"],
"amount": [5.0, 15.0, 25.0, 8.0, 30.0, 12.0],
"event_ts": [
dt.datetime(2026, 9, 1, 14, 22, 5),
dt.datetime(2026, 9, 1, 15, 2, 41),
dt.datetime(2026, 9, 2, 9, 14, 33),
dt.datetime(2026, 9, 2, 10, 44, 18),
dt.datetime(2026, 9, 3, 18, 3, 47),
dt.datetime(2026, 9, 3, 19, 58, 22),
],
}))
print(f"seeded {ns}.raw_events: 6 rows")

Run it:

Terminal window
python seed.py

If you get ACCESS_DENIED, your profile doesn’t have S3 write access to the bucket — check the bucket policy or use a different bucket.

Terminal window
reble run --refresh

No model list needed — --refresh builds everything that isn’t built yet. On a first run, that’s all of your models, whether you have two or two hundred. You’ll see:

scope (edited) 2 mart_orders, stg_orders
pinned inputs 1 reble_pin__local__raw_events
engine duckdb (local)
stg_orders: ran (4 rows, ...)
mart_orders: ran (4 rows, ...)

Two Iceberg tables now exist in your S3 bucket, registered in Glue, built in the right order (Reble read the dependency from the SQL — you didn’t configure it). main — production — now has your tables.

Step 4: change a model — production stays untouched

Section titled “Step 4: change a model — production stays untouched”

Now the part you came for. Edit models/stg_orders.sql: change amount > 10 to amount > 20. Then:

Terminal window
reble run
reble diff mart_orders

Reble noticed your edit and rebuilt that model and every model built from it — on a separate data branch. Your main tables are exactly as they were. Don’t like the change? Edit again, or walk away — main never knew.

reble diff shows what your change produced — rows, not SQL:

analytics_test.mart_orders: +0 -2 ~0 (2 unchanged)

Two orders fell below the new threshold — you see exactly which rows leave production before anything moves.

This is your review artifact before anything reaches production.

Terminal window
reble status # did anything move underneath you while you worked?
reble promote # apply the branch's tables to main

promote is the only step that touches main — and it’s careful about it: if the input data changed under your feet, it rebuilds your change on the fresh data, shows you the new diff, and only then applies. It never silently mixes your change with someone else’s data.

Terminal window
aws glue delete-table --database-name analytics_test --name raw_events
aws glue delete-table --database-name analytics_test --name stg_orders
aws glue delete-table --database-name analytics_test --name mart_orders
aws glue delete-database --name analytics_test
aws s3 rm s3://reble-test-${ACCOUNT_ID}/reble --recursive

Cost of the whole walkthrough: cents.

The walkthrough ran on your laptop — fine for a tutorial, wrong for a nightly job. In production the same commands run from whatever already schedules your work. With Airflow, the refresh becomes one task:

ingest >> BashOperator(
task_id="reble_refresh",
bash_command="cd /srv/warehouse && reble run --refresh",
)

Install reble[aws] in the worker image, pass credentials the way you already do, and the refresh scopes itself from whatever the ingest just landed. The full pattern book is in the Airflow guide.

  • reble --version looks old — cached package. Force-reinstall: pip install --force-reinstall 'reble[aws]'
  • You must specify a region — set export AWS_DEFAULT_REGION=us-east-1, or put region under the catalog in reble.yml (translated to glue.region automatically since 0.5.1)
  • Unable to locate credentialsexport AWS_PROFILE=orchestra and verify with aws sts get-caller-identity
  • ACCESS_DENIED on S3 — the bucket policy doesn’t allow your IAM user/role to write. Use a bucket you own or update the policy.
  • input 'raw_events' not found — run seed.py first (Step 2)
  • No such option '--refresh' — you have an old package version; see the first item above.

Exit codes (3 drift, 4 promote blocked, 7 missing diff key) are not AWS-specific — see Exit codes and JSON output.