Chapter 2 · Foundations

The Lab

Lab:Ubuntu 24.04 VM on AWS+Docker · docker-compose+SQL Server 2022 on Linux (container)+Airflow 2.7 (containers)This chapter is the infrastructure: the Linux VM in AWS, Docker networking and bind mounts, and SQL Server 2022 running inside a Linux container.

Everything in this tutorial runs on one small Ubuntu Linux virtual machine deployed in AWS, with Docker: SQL Server 2022 for Linux in one container, and an Airflow stack in six more. This chapter shows what runs where, how the folders map into the containers, and how to rebuild the warehouse from nothing.

Airflow containers SQL Server container Host folders (bind mounts)
UBUNTU 24.04 SERVER · 192.168.10.150 · 2 vCPU · 9.5 GB RAM · Docker + docker-compose v1 Host folders ~/airflow/ dags/ 2 SBEcoLTDDW DAGs plugins/ sbecoltddw_operators.py sql/ sbecoltddw/**.sql (new) logs/ task logs docker-compose.yaml (Version10) ~/sbecoltddw/ course build, sources, captures Airflow 2.7.2 · CeleryExecutor webserver :8080 UI scheduler parses DAGs worker runs the tasks triggerer deferrables postgres 13 Airflow metadata redis Celery broker providers: common-sql 1.7.2 · odbc 4.0.0 pyodbc 4.0.39 · ODBC Driver 18 for SQL Server mounts: /opt/airflow/{dags,plugins,sql,logs} SQL Server 2022 Developer (CU27) · container labenv_mssql_1 · port 1433 AdventureWorks2022 sales OLTP · the source 19,820 customers 31,465 orders Other samples not used by the pipeline tempdb · msdb system databases SBEcoLTDDW the warehouse Fact · Dimension · stg rpt · audit · etl columnstore · 2 ind. views ODBC · host "mssql" · 1433 Airflow connections mssql_sbecoltddw_etl DataLoader @ SBEcoLTDDW · every step mssql_adventureworks sa @ AdventureWorks2022 · simulator DAG Inside the Linux container /var/opt/mssql/data (Docker volume) sqlcmd via docker exec -i MSSQL_MEMORY_LIMIT_MB = 2048
Figure 2.1 — The lab. All containers share one Docker network, so Airflow reaches SQL Server by the service name mssql.

What runs where

ComponentVersionNotes
DWSQL Server2022 Developer, CU27 (16.0.4295), on LinuxImage mcr.microsoft.com/mssql/server:2022-latest, memory capped at 2 GB, data in a Docker volume
AFAirflow2.7.2, CeleryExecutorCustom image (adds dbt for another lab), Postgres metadata DB, Redis broker
PYPython librariespyodbc 4.0.39, ODBC Driver 18Already in the image; the providers common-sql and odbc are installed too
OSServerUbuntu 24.04 VM in AWS, 2 vCPU, 9.5 GB RAMShared with other labs, so every step is sized to be modest

The folder layout: logic and orchestration apart

The one rule of this design is that no SQL lives in the DAG. Compose Version10 added a single line (Version11 only renamed comments), a new bind mount, so the SQL folder appears inside every Airflow container next to the DAGs:

  volumes:
    - ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags
    - ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs
    - ${AIRFLOW_PROJ_DIR:-.}/config:/opt/airflow/config
    - ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins
    - ${AIRFLOW_PROJ_DIR:-.}/dbt:/opt/airflow/dbt
    - ${AIRFLOW_PROJ_DIR:-.}/sql:/opt/airflow/sql      # Version10: plain SQL for SBEcoLTDDW
dags/
  sbecoltddw_e2e_pipeline.py     # the pipeline (orchestration only)
  sbecoltddw_source_changes.py   # source-system simulator
plugins/
  sbecoltddw_operators.py        # MsSqlFileOperator
sql/sbecoltddw/
  00_control/      start_run, end_run, fail_run
  01_deploy/       11 idempotent DDL files
  02_extract/      source → stg
  03_stage_checks/ stop early on bad staging
  04_dimensions/   Date, Customer SCD2
  05_facts/        Orders MERGE
  06_post_load/    columnstore, stats, view check
  07_dq/           the data quality gate
  08_publish/      watermark, run summary
  90_source_changes/  the simulator's SQL
run_sbecoltddw.sh        # course build (Chapters 2–6 of the course)
reset_source.sh          # undo the simulator's source changes
sql/                     # course scripts, 99_Verify
airflow/                 # master copy of dags, plugins, sql
  setup_airflow_sbecoltddw.sh
  sbdw_run.sh             # trigger a DAG, wait, print states
  sbdw_clear.sh           # clear a failed task + downstream
  sbdw_capture.sh         # pull SQL output out of task logs
  sbdw_scenarios.sh       # the Chapter 12 scenarios
captures/                # outputs quoted in this tutorial
.dataloader.env          # DataLoader password (chmod 600)

Connections and logins

Airflow connectionSQL loginUsed byWhy a separate one
mssql_sbecoltddw_etlDataLoader (CONTROL on SBEcoLTDDW, read on AdventureWorks2022)Every pipeline stepResource Governor puts this login in the DataLoads group (Chapter 9)
mssql_adventureworkssaThe simulator DAGIt plays the source application, so it writes to AdventureWorks2022

Lab shortcut

Using sa for the simulator is a lab convenience: it plays the source application. The pipeline itself never uses sa. The DataLoader password is generated on the server and never written into a DAG, a log or this site.

Rebuilding everything from nothing

  1. Course objects, empty. REBUILD=1 COURSE_ONLY=1 bash ~/sbecoltddw/run_sbecoltddw.sh drops and recreates SBEcoLTDDW with the course's schemas, tables, indexed view and columnstore index, plus the DataLoader login with its Resource Governor setup. Every statement is run by sqlcmd inside the SQL Server Linux container. course-build/run_sbecoltddw.sh · ~30 s
  2. Wire up Airflow. bash ~/sbecoltddw/airflow/setup_airflow_sbecoltddw.sh backs up the compose file, installs Version11, copies the SQL folder, plugin and DAGs, recreates the Airflow containers and creates the connection. airflow/setup_airflow_sbecoltddw.sh · ~2 min
  3. Load. ~/sbecoltddw/airflow/sbdw_run.sh sbecoltddw_e2e_pipeline triggers the DAG and waits. The first run deploys the pipeline objects and loads everything. ~1 min per run
echo ">> 3. recreate Airflow containers (docker-compose v1: rm -sf, then up -d)"
SERVICES="airflow-webserver airflow-scheduler airflow-worker airflow-triggerer"
docker-compose rm -sf $SERVICES
docker-compose up -d $SERVICES
echo "waiting for the scheduler..."
for i in $(seq 1 30); do
  if docker exec airflow_airflow-scheduler_1 test -d /opt/airflow/sql/sbecoltddw 2>/dev/null &&
     docker exec airflow_airflow-scheduler_1 airflow version >/dev/null 2>&1; then break; fi
  sleep 5
done
docker ps --format '{{.Names}}  {{.Status}}' | grep airflow_

echo ">> 4. connections (passwords are read from files/connections, never printed)"
set -a; source "$HOME/sbecoltddw/.dataloader.env"; set +a
docker exec -i -e DL_PASSWORD airflow_airflow-scheduler_1 python - <<'PY'
import os
from airflow.hooks.base import BaseHook
from airflow.models import Connection
from airflow.settings import Session

wanted = [
    Connection(conn_id="mssql_sbecoltddw_etl", conn_type="odbc", host="mssql", port=1433,
               schema="SBEcoLTDDW", login="DataLoader", password=os.environ["DL_PASSWORD"],
               description="SBEcoLTDDW pipeline steps (Resource Governor group DataLoads)"),
]
s = Session()
for c in wanted:
    s.query(Connection).filter(Connection.conn_id == c.conn_id).delete()
    s.add(c)
    print(f"connection {c.conn_id}: {c.conn_type}://{c.login}@{c.host}:{c.port}/{c.schema}")
s.commit()
PY

Key takeaways

  • One AWS VM, two container groups: Airflow (6 containers) and SQL Server (1), on one Docker network.
  • The SQL folder is mounted next to the DAGs, which keeps logic and orchestration in separate files.
  • Two connections, one per job: the ETL login for the pipeline, and the source system itself for the simulator.
  • The whole warehouse can be rebuilt with three commands.