Skill 62 · Authoring Mwaa Workflow
Subchapter 62.1
references/authoring-provisioned-dag.mdMarkdown14 KBView on GitHub
Detailed steps for the provisioned / Python-DAG path. The agent is routed here from the authoring-mwaa-workflow SKILL.md “Step 0: Route to Path”; it does not need the Serverless (Path B) reference.
See dag-patterns.md for templates and examples.
@daily/@hourly (or any @-preset) unless the
user explicitly asks for that exact expression. If the user has no
preference, derive a spread off-peak cron from the dag_id (minute 1-59,
hour 1-6 UTC; see A6 rule 15) — do not reuse a single fixed time across DAGs.Ask for target environment — “Do you have an existing MWAA environment for this DAG?” Skip if already provided or user said “create new.”
If environment exists: a. Fetch config:
aws mwaa get-environment --name <env-name>Extract: Airflow version (overrides A1 if different), Python version, SourceBucketArn, RequirementsS3Path, RequirementsS3ObjectVersion, PluginsS3Path, PluginsS3ObjectVersion, StartupScriptS3Path, StartupScriptS3ObjectVersion. b. Fetch requirements.txt from S3 using the exact version in use:
If RequirementsS3ObjectVersion is present (bucket has versioning):
aws s3api get-object --bucket <bucket-name-from-SourceBucketArn> --key <RequirementsS3Path> \
--version-id <RequirementsS3ObjectVersion> /dev/stdoutIf RequirementsS3ObjectVersion is absent (no versioning):
aws s3 cp s3://<bucket-name-from-SourceBucketArn>/<RequirementsS3Path> -Skip asking user to provide if fetched. Ask only if path is empty or
file missing.
c. Fetch startup script (if StartupScriptS3Path is set):
aws s3api get-object --bucket <bucket> --key <StartupScriptS3Path> --version-id <StartupScriptS3ObjectVersion> /dev/stdoutaws s3 cp s3://<bucket>/<StartupScriptS3Path> -
Record which system packages are installed (yum/apt lines). Some
Python packages in requirements.txt depend on system libraries
(e.g., psycopg2 needs libpq-devel, pyodbc needs
unixODBC-devel).
d. Inspect plugins.zip (if PluginsS3Path is set):--version-id if versioned).unzip -l /tmp/plugins.zip.whl files — these provide Python packages
outside of requirements.txt. Record their package names and
versions for A4 validation.aws mwaa invoke-rest-api --name <env-name> --method GET --path <path>):/connections — configured external systems/variables — parameterized values/pools — concurrency configuration/dags — existing DAGs (for cross-DAG triggering)/datasets (Airflow 2.4–2.x) or /assets (3.0+) — event
dependencies. Skip if version < 2.4.Each of these is a paginated collection (Airflow REST default limit
100). On a large environment pass
--query-parameters '{"limit":"100","offset":"0"}' and re-issue with
offset incremented by 100 until you have read total_entries items,
so introspection does not miss a connection/variable/DAG the new DAG
references beyond the first 100.
If no environment — ask user to provide requirements.txt or confirm starting fresh. Skip all introspection.
How introspection informs downstream steps:
tomorrow_ds
removed in 3.x → use macros.ds_add(ds, 1)).Validate that the environment’s installed packages support the operators
chosen in A3. The effective package set is the union of requirements.txt
AND any .whl files bundled in plugins.zip. Skip entirely for Path B
(Serverless has a fixed package set).
https://raw.githubusercontent.com/apache/airflow/constraints-{airflow_version}/constraints-{python_version}.txt.whl files (name + version extracted
from the wheel filename: {name}-{version}-*.whl).psycopg2 → libpq-devel, pyodbc → unixODBC-devel,
cryptography → openssl-devel), verify the startup script
installs them.<package> needs
<library> installed via startup script.”Skip conditions:
Validate that the MWAA execution role grants access to all AWS resources the DAG will interact with at runtime. Skip if no environment exists yet: inline creation (see deploying-mwaa.md) builds the role scoped to exactly the DAG’s Action/Resource needs, so coverage is established at role-build time and a separate A5 pass isn’t required.
ExecutionRoleArn (the segment
after role/; for path-qualified roles, include the path, e.g.,
path/role-name). Do NOT pass the full ARN to --role-name.
aws iam list-attached-role-policies --role-name <role-name>aws iam get-policy --policy-arn <arn> then
aws iam get-policy-version --policy-arn <arn> --version-id <default-version>aws iam list-role-policies --role-name <role-name> (inline policies)aws iam get-role-policy --role-name <role-name> --policy-name <name>
for each inline policyget-role-policy), then put-role-policy with a document that PRESERVES
all existing statements and appends only the new pairs (it overwrites the
whole named policy). Restate the shared-role blast radius in the confirmation.dagrun_timeout >= sum(service timeouts + retries).
Every task gets execution_timeout. Sensors use their own timeout.deferrable=True on every supporting operator.
Only available in Airflow 2.7+. Do NOT use for versions before 2.7.retry_exponential_backoff=True + max_retry_delay.from airflow... imports
and constant definitions at module level. All boto3/json/logic inside
functions. Top-level code degrades scheduler parsing.while True + sleep inside a
task. Use the deferrable operator, or split into submit + @task sensor..expand(): use dynamic task mapping for parallel work.
Never create tasks inside a Python loop (degrades parsing).dags/.
SQL, YAML, config, scripts go elsewhere (e.g., scripts/, sql/).
Use .airflowignore to exclude non-DAG files from parsing.on_failure_callback and similar publish dag_id, task_id,
start_date, end_date, log_url, and the exception TYPE only (never the
message/traceback — it can carry secrets; CWE-532). Import boto3 inside the
callback.{{ ds_nodash }}).verbose=True: floods CloudWatch unless explicitly asked.aws_conn_id: omit — uses default connection.@task functions: always include.@daily, @hourly, or any @-preset
unless the user explicitly requests that exact expression (presets bunch
every DAG at midnight / top-of-hour and congest the scheduler). When the
user gives no preferred time, DERIVE a spread off-peak cron from the
dag_id so each DAG lands in its own slot — never reuse one fixed time
across DAGs. Do this at authoring time — you pick the value and write a
literal cron into the DAG (no top-level logic in the DAG file, per rule 4),
so no runtime library is needed. Turn the dag_id into a stable integer
n with this one fixed rule — always the same function, so the same
dag_id derives the same n, and therefore the same slot, across every
authoring session: a position-weighted sum
n = sum(i * ord(c) for i, c in enumerate(dag_id, 1)) (order-sensitive, so
word-order variants of a dag_id do not collide on one slot; needs no
runtime library and always works). Do NOT substitute a different function
(e.g. zlib.crc32, or Python’s built-in hash(), which is also salted per
process) — a different function yields a different n and a different cron
for the same dag_id, breaking reproducibility. Then minute = 1 + (n % 59) (1-59,
non-zero), hour = 1 + ((n // 59) % 6) (1-6 UTC), and
schedule = "{minute} {hour} * * *". If you cannot compute n for any
reason, still pick a varied off-peak time yourself (minute 1-59, hour 1-6
UTC) per DAG — NEVER give up, skip the schedule, fall back to a single fixed
time, or use a @-preset.catchup=False with a fixed past start_date: always set catchup=False
unless the user explicitly wants historical backfill. On Airflow 2.x catchup
defaults to True, so a fixed past start_date (see dag-patterns.md) plus a
schedule backfills every missed interval in one burst when the DAG is
unpaused. AF3 defaults it False; set it explicitly so behavior matches on both.imports (airflow only at top)
constants
callback function
default_args
DAG context manager
tasks with dependencies at end