Deploying DAGs
databaas dag deploy uploads a repo's Airflow code to the environment's DAG bucket, where
Airflow picks it up. It runs in CI, with the environment's DAG deployment key from the
repository secrets. It needs no databaas login.
pip install "databaas-utils[deploy]"
databaas dag validate -t prod # with Airflow and the DAGs' dependencies installed
databaas dag deploy -t prod
CI only, for now
The command authenticates with a long-lived S3 key, which only CI has. Run on a laptop,
it stops with an error asking for AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
Deploying with your own login is coming.
Configuration
databaas.yml at the repo root. The command finds it from the working directory or any
parent; --config PATH points at another one.
bundle:
name: marine-pipeline # lower-case letters, digits and hyphens
targets:
dev:
mode: development
prod:
mode: production
artifacts:
dags: airflow/ # the directory that is deployed
| Key | |
|---|---|
bundle.name |
Names the deployment in the bucket. Starts with a letter. |
targets.<name>.mode |
development or production. At most one production target. |
targets.<name>.default |
Optional. true lets you leave out -t. |
artifacts.dags |
The directory to upload, relative to databaas.yml. |
Other keys are ignored.
Commands
| Command | Does |
|---|---|
databaas dag validate -t TARGET |
Parse the target's DAGs with Airflow, exactly as they would be deployed |
databaas dag deploy -t TARGET |
Make the target's prefix hold exactly the artifacts.dags directory |
databaas dag deploy -t TARGET --dry-run |
List what would be uploaded and deleted, change nothing |
databaas dag destroy -t TARGET |
Delete everything under the target's prefix. Production needs --force. |
All three take --dev-key KEY and --config PATH. deploy and destroy also take
--bucket NAME and --endpoint-url URL. deploy -v prints every upload; validate -v
prints Airflow's output.
Validating before a deploy
databaas dag validate stages the directory exactly as deploy would, renamed packages
and _databaas_target.json included, and parses it with Airflow's DagBag. The staged
directory is appended to sys.path, as in the cluster, and from databaas_target import
target resolves to this package. It fails on:
- a file that does not import, including Airflow's own checks such as a duplicate dag id or one longer than 250 characters;
- no DAGs at all;
- on a development target, a dag id without the
dev_<dev-key>_prefix. Such a DAG would replace the production DAG of the same id; build ids withtarget.dag_id(...).
Problems are reported against the file in the repo, not the staged copy. In GitHub Actions they also appear as annotations on the file.
It needs no key, but it runs the DAG files, so Airflow and the DAGs' own dependencies
must be installed in the same Python as databaas-utils. Without Airflow it says so.
Pin the Airflow version the environment runs, and install it with
its constraints file.
What a deploy does
Each target is one prefix in the environment's metadata bucket:
s3://<bucket>/dags/prod/<bundle>/ production
s3://<bucket>/dags/dev/<dev-key>/<bundle>/ development
A deploy works like aws s3 sync --delete. It uploads files that changed and deletes
files that are no longer in the directory. It never touches anything outside its own
prefix. __pycache__, *.pyc, .git, .venv and .DS_Store are not uploaded, and
symlinks are refused.
Every deploy writes a _databaas_target.json next to the code. It records the mode, dev
key, bundle, repo, branch, commit, CI run and time.
target_for and scoped() read it, so a dev
deploy's DAG ids become dev_<dev-key>_<name>. See
Dev deployments.
The directory is copied to a temporary folder first, so the checkout is never changed. That matters for development targets, which are adjusted before upload as described below.
Development targets
The dev key is the branch name as a slug: lower case, with anything other than letters,
digits and - replaced by -. A branch longer than 40 characters is shortened, with a
hash of the full name added. CI provides the branch: GITHUB_HEAD_REF on a GitHub pull
request, BITBUCKET_BRANCH on Bitbucket. Elsewhere it comes from git. --dev-key
overrides it, and is turned into a slug the same way.
Airflow puts every deployed directory on one shared sys.path. If two open branches both
ship a package called marine_pipeline, either copy can win an import. So a dev deploy
renames the top-level packages and modules in artifacts.dags to
<name>_<dev_key> and rewrites the imports of them. On branch feature/x:
| In the repo | Deployed |
|---|---|
marine_pipeline/ |
marine_pipeline_feature_x/ |
helpers.py |
helpers_feature_x.py |
from marine_pipeline.tasks import run |
from marine_pipeline_feature_x.tasks import run |
dags/ and dotfiles keep their names. Relative imports are left as they are, and so are
directories without Python files, such as dbt/. Entries in .airflowignore are renamed
too. If a file does not parse, the deploy stops before anything is uploaded.
Production deploys upload the directory exactly as it is.
Credentials
| Variable | |
|---|---|
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
The environment's DAG deployment key. Required. |
AWS_ENDPOINT_URL |
The S3 endpoint, e.g. https://s3.nl-ams.scw.cloud. Or --endpoint-url. |
DATABAAS_DAGS_BUCKET |
The environment's metadata bucket. Or --bucket. |
AWS_DEFAULT_REGION |
Optional, default nl-ams. |
Only these variables are read. ~/.aws and a databaas login are ignored, so a deploy
uses exactly the key the pipeline was given. A key from another environment is refused
by the bucket with a plain 403.
CI
GitHub Actions, deploying pull requests as dev targets, main as production, and
removing a dev target when its pull request closes:
# .github/workflows/deploy-dags.yaml
on:
pull_request:
types: [opened, synchronize, reopened, closed]
push:
branches: [main]
env:
AWS_ACCESS_KEY_ID: ${{ secrets.SCW_DAGS_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCW_DAGS_SECRET_KEY }}
AWS_ENDPOINT_URL: https://s3.nl-ams.scw.cloud
AWS_DEFAULT_REGION: nl-ams
DATABAAS_DAGS_BUCKET: databaas-demo-metadata
AIRFLOW_CONSTRAINTS: https://raw.githubusercontent.com/apache/airflow/constraints-3.1.8/constraints-3.12.txt
jobs:
deploy:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
concurrency: { group: "dags-${{ github.head_ref || 'prod' }}", cancel-in-progress: false }
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: |
target=${{ github.event_name == 'push' && 'prod' || 'dev' }}
uvx --from 'databaas-utils[deploy]' --with apache-airflow==3.1.8 \
-c "$AIRFLOW_CONSTRAINTS" --with-requirements requirements.txt \
databaas dag validate -t "$target"
uvx --from 'databaas-utils[deploy]' databaas dag deploy -t "$target"
cleanup:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uvx --from 'databaas-utils[deploy]' databaas dag destroy -t dev
Pull requests from forks get no secrets, so their deploy stops at the key check.
requirements.txt holds what the DAG files import at parse time. Airflow's constraints
file keeps its dependencies at versions it works with; without it, a newer SQLAlchemy can
break the Airflow import. Steps that build files
into the DAG directory, such as dbt parse for a Cosmos manifest, go before both
commands. deploy uploads the directory; only validate runs your DAG files.
Bitbucket Pipelines work the same, with the key and AIRFLOW_CONSTRAINTS in repository
variables:
# bitbucket-pipelines.yml
image: python:3.12
pipelines:
pull-requests:
"**":
- step:
script:
- pip install 'databaas-utils[deploy]' apache-airflow==3.1.8 -r requirements.txt -c "$AIRFLOW_CONSTRAINTS"
- databaas dag validate -t dev
- databaas dag deploy -t dev
branches:
main:
- step:
script:
- pip install 'databaas-utils[deploy]' apache-airflow==3.1.8 -r requirements.txt -c "$AIRFLOW_CONSTRAINTS"
- databaas dag validate -t prod
- databaas dag deploy -t prod
Bitbucket cannot start a pipeline when a pull request closes. Remove a dev target with
databaas dag destroy -t dev --dev-key <dev-key>, for example from a manual pipeline.
Coming: deploying through Lakekeeper
DAG code will move behind Lakekeeper, like the rest of the lakehouse. A deploy will then be
signed by Lakekeeper rather than made with an S3 key, so people can deploy their own dev
target after databaas login and CI can deploy production as a machine account, without
long-lived keys in repository secrets. Who may deploy where will be a Lakekeeper grant.
Until then, databaas dag deploy with the environment's DAG deployment key is the
supported way to deploy, from CI.