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.
Prerequisites
Section titled “Prerequisites”1. Python 3.10–3.13
Section titled “1. Python 3.10–3.13”Python 3.14 is not yet tested. Check with python3 --version. If you’re
on 3.14, create a 3.13 venv:
python3.13 -m venv reble-test && source reble-test/bin/activate2. Install Reble (the right version)
Section titled “2. Install Reble (the right version)”pip install 'reble[aws]'reble --versionIf the version looks older than the one on
PyPI, you have a cached package:
pip install --force-reinstall 'reble[aws]'.
3. An AWS profile that works
Section titled “3. An AWS profile that works”Verify credentials and region are configured:
aws configure list # should show an access key and a regionaws sts get-caller-identity # should return your account IDIf not, set them up:
aws configure set aws_access_key_id YOUR_KEY --profile orchestraaws configure set aws_secret_access_key YOUR_SECRET --profile orchestraaws configure set region us-east-1 --profile orchestraExport for this session:
export AWS_PROFILE=orchestraexport AWS_DEFAULT_REGION=us-east-14. An S3 bucket you can write to
Section titled “4. An S3 bucket you can write to”If you don’t have one, create it:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)aws s3api create-bucket --bucket "reble-test-${ACCOUNT_ID}" --region us-east-1echo "bucket: reble-test-${ACCOUNT_ID}"Note the bucket name — you’ll put it in the config below.
Step 1: create the project
Section titled “Step 1: create the project”mkdir my-warehouse && cd my-warehouse && mkdir modelsCreate two SQL files — a model is one SQL file that produces one table, and the file name is the table name:
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 typedwhere status = 'paid' and amount > 10EOF
cat > models/mart_orders.sql <<'EOF'-- kind: table-- key: order_idselect 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_datefrom stg_ordersEOFNow create reble.yml — replace reble-test-YOUR_ACCOUNT_ID with your
bucket name:
version: 1warehouse: catalog: type: glue region: us-east-1 warehouse: s3://reble-test-YOUR_ACCOUNT_ID/reble namespace: analytics_test default_base: mainbranching: git_sync: false # standalone: this project has no git repolineage: models_path: modelsStep 2: seed one input table
Section titled “Step 2: seed one input table”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:
import datetime as dt
import pyarrow as paimport yamlfrom 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:
python seed.pyIf you get ACCESS_DENIED, your profile doesn’t have S3 write access to
the bucket — check the bucket policy or use a different bucket.
Step 3: build production (once)
Section titled “Step 3: build production (once)”reble run --refreshNo 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_orderspinned inputs 1 reble_pin__local__raw_eventsengine 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:
reble runreble diff mart_ordersReble 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.
Step 5: promote
Section titled “Step 5: promote”reble status # did anything move underneath you while you worked?reble promote # apply the branch's tables to mainpromote 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.
Step 6: clean up
Section titled “Step 6: clean up”aws glue delete-table --database-name analytics_test --name raw_eventsaws glue delete-table --database-name analytics_test --name stg_ordersaws glue delete-table --database-name analytics_test --name mart_ordersaws glue delete-database --name analytics_testaws s3 rm s3://reble-test-${ACCOUNT_ID}/reble --recursiveCost of the whole walkthrough: cents.
Run it in production
Section titled “Run it in production”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.
Troubleshooting
Section titled “Troubleshooting”reble --versionlooks old — cached package. Force-reinstall:pip install --force-reinstall 'reble[aws]'You must specify a region— setexport AWS_DEFAULT_REGION=us-east-1, or putregionunder the catalog inreble.yml(translated toglue.regionautomatically since 0.5.1)Unable to locate credentials—export AWS_PROFILE=orchestraand verify withaws sts get-caller-identityACCESS_DENIEDon 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— runseed.pyfirst (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.