Why This Matters
Amazon MWAA is the managed orchestration layer that ties together the entire AWS data ecosystem ā S3, Glue, Redshift, EMR, Lambda, and Step Functions. Mastering MWAA means understanding DAG design patterns, Celery executor scaling, Secrets Manager integration, and the trade-offs between MWAA, Step Functions, and Glue Workflows. Interviewers expect you to design production DAGs with proper error handling, idempotency, and cost optimization ā not just import operators and hope for the best.
MWAA Architecture
Real-World Project Structure
A production MWAA deployment follows a disciplined directory and pipeline structure:
mwaa-production-platform/
āāā infrastructure/
ā āāā terraform/
ā ā āāā mwaa.tf # MWAA environment config
ā ā āāā vpc.tf # Private subnets + VPC endpoints
ā ā āāā iam.tf # Execution role + task roles
ā ā āāā s3.tf # DAG bucket + versioning
ā ā āāā cloudwatch.tf # Log groups + metric alarms
ā āāā cloudformation/
ā āāā mwaa-environment.yaml
āāā dags/
ā āāā __init__.py
ā āāā etl/
ā ā āāā daily_sales_etl.py
ā ā āāā customer_dimension.py
ā ā āāā inventory_sync.py
ā āāā streaming/
ā ā āāā kinesis_consumer.py
ā ā āāā kafka_to_s3.py
ā āāā ml/
ā ā āāā feature_pipeline.py
ā ā āāā model_training.py
ā āāā quality/
ā āāā data_validation.py
ā āāā anomaly_checks.py
āāā plugins/
ā āāā __init__.py
ā āāā custom_operators/
ā ā āāā redshift_copy.py
ā ā āāā glue_trigger.py
ā āāā hooks/
ā āāā s3_hook_extended.py
āāā tests/
ā āāā unit/
ā ā āāā test_daily_sales_etl.py
ā ā āāā test_validations.py
ā āāā integration/
ā āāā test_full_pipeline.py
āāā requirements.txt
āāā airflow.cfg
āāā .airflowignore
DAG Design for Production Pipelines
from datetime import datetime, timedelta
from airflow import DAG
from airflow.providers.amazon.aws.operators.glue import GlueJobOperator
from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor
from airflow.providers.amazon.aws.operators.redshift_data import RedshiftDataOperator
from airflow.operators.python import PythonOperator
from airflow.operators.empty import EmptyOperator
from airflow.utils.task_group import TaskGroup
import logging
logger = logging.getLogger(__name__)
default_args = {
'owner': 'data-engineering',
'depends_on_past': False,
'email_on_failure': True,
'email': ['data-team@company.com'],
'retries': 3,
'retry_delay': timedelta(minutes=5),
'retry_exponential_backoff': True,
'max_retry_delay': timedelta(minutes=30),
'execution_timeout': timedelta(hours=2),
}
def validate_row_count(**context):
"""Validate that the ETL produced expected row counts."""
ti = context['ti']
row_count = ti.xcom_pull(task_ids='transform.run_etl_job', key='row_count')
if row_count is None or row_count < 100:
raise ValueError(f"Insufficient rows processed: {row_count}")
logger.info(f"Row count validation passed: {row_count}")
def validate_null_checks(**context):
"""Run null checks on critical columns."""
import boto3
redshift = boto3.client('redshift-data')
result = redshift.execute_statement(
ClusterIdentifier='analytics-cluster',
Database='dev',
Sql="SELECT COUNT(*) FROM analytics.staging WHERE user_id IS NULL"
)
# Check result
logger.info("Null check validation completed")
with DAG(
'daily_etl_pipeline',
default_args=default_args,
description='Production ETL: extract, transform, load, validate',
schedule_interval='0 2 * * *',
start_date=datetime(2024, 1, 1),
catchup=False,
max_active_runs=1,
tags=['etl', 'production', 'data-lake'],
doc_md="""
## Daily ETL Pipeline
- **Extract**: Waits for raw data in S3 landing zone
- **Transform**: Runs Glue ETL job on partitioned data
- **Load**: Inserts transformed data into Redshift
- **Validate**: Runs data quality checks
""",
) as dag:
start = EmptyOperator(task_id='start')
with TaskGroup('extract') as extract_group:
wait_for_raw = S3KeySensor(
task_id='wait_for_raw_data',
bucket_name='data-lake-raw',
bucket_key='landing/{{ ds }}/',
wildcard_match=True,
timeout=3600,
poke_interval=60,
mode='reschedule',
)
with TaskGroup('transform') as transform_group:
run_glue = GlueJobOperator(
task_id='run_etl_job',
job_name='daily-etl-transform',
script_location='s3://glue-scripts/etl/transform.py',
s3_bucket='glue-scripts',
iam_role_name='GlueETLRole',
create_job_kwargs={
'GlueVersion': '4.0',
'NumberOfWorkers': 10,
'WorkerType': 'G.1X',
},
job_poll_interval=30,
aws_conn_id='aws_default',
)
with TaskGroup('load') as load_group:
load_redshift = RedshiftDataOperator(
task_id='load_to_redshift',
cluster_identifier='analytics-cluster',
database='dev',
db_user='admin',
sql="""
COPY analytics.fact_daily
FROM 's3://data-lake-processed/{{ ds }}/'
IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftLoadRole'
FORMAT AS PARQUET;
""",
aws_conn_id='aws_default',
)
with TaskGroup('validate') as validate_group:
check_row_count = PythonOperator(
task_id='validate_row_count',
python_callable=validate_row_count,
)
check_nulls = PythonOperator(
task_id='validate_null_checks',
python_callable=validate_null_checks,
)
end = EmptyOperator(task_id='end')
start >> extract_group >> transform_group >> load_group >> validate_group >> end
Airflow Operators for AWS
from airflow.providers.amazon.aws.operators.glue import GlueJobOperator
from airflow.providers.amazon.aws.sensors.glue import GlueJobSensor
from airflow.providers.amazon.aws.operators.lambda_ import LambdaInvokeFunctionOperator
from airflow.providers.amazon.aws.operators.ecs import EcsRunTaskOperator
# Pattern 1: Launch Glue and wait for completion
launch_glue = GlueJobOperator(
task_id='launch_etl',
job_name='my-etl-job',
script_location='s3://scripts/etl.py',
s3_bucket='my-bucket',
iam_role_name='GlueRole',
create_job_kwargs={'NumberOfWorkers': 10},
aws_conn_id='aws_default',
)
wait_for_glue = GlueJobSensor(
task_id='wait_for_etl',
job_name='my-etl-job',
aws_conn_id='aws_default',
poke_interval=30,
timeout=3600,
mode='reschedule',
)
# Pattern 2: Lambda with payload and response handling
invoke_lambda = LambdaInvokeFunctionOperator(
task_id='process_record',
function_name='process-data',
payload={'bucket': 'my-bucket', 'key': 'data/file.parquet'},
result_path='$.statusCode',
aws_conn_id='aws_default',
)
# Pattern 3: ECS Fargate for custom containers
run_container = EcsRunTaskOperator(
task_id='run_custom_processor',
cluster='data-cluster',
task_definition='processor-task',
launch_type='FARGATE',
network_configuration={
'awsvpcConfiguration': {
'subnets': ['subnet-12345'],
'securityGroups': ['sg-12345'],
'assignPublicIp': 'DISABLED',
}
},
aws_conn_id='aws_default',
)
Mathematical Formulas
MWAA vs Step Functions vs Glue Workflows
| Criteria | MWAA | Step Functions | Glue Workflows |
|---|---|---|---|
| Complexity | High (Python DAGs) | Medium (JSON/YAML) | Low (visual) |
| Services | Any AWS + external | AWS-native only | Glue + limited |
| Cost Model | Always-on (per hour) | Pay-per-execution | Per DPU-hour |
| Scaling | Worker auto-scaling | Automatic | Automatic |
| Error Handling | Retry, callbacks, SLAs | Built-in catch/retry | Basic |
| Monitoring | Airflow UI + CloudWatch | Console + CloudWatch | Glue Console |
| Learning Curve | Steep (Airflow concepts) | Moderate | Low |
| Community | 800+ operators | AWS-only | None |
| Best For | Complex branching, cross-service | Lambda-heavy, event-driven | Glue ETL orchestration |
Performance Considerations
| Metric | Recommended | Impact |
|---|---|---|
| Workers (min) | 2 (HA minimum) | Fault tolerance |
| Workers (max) | Based on peak queue | Cost vs. throughput |
| Scheduler Heartbeat | 5 seconds (default) | Task submission frequency |
| DAG Sync Interval | 5 minutes (default) | Deployment latency |
| Sensor Mode | reschedule | Frees worker slots between pokes |
| Task Retries | 2-3 with exponential backoff | Transient failure recovery |
| Execution Timeout | Set per operator | Prevents runaway tasks |
max_active_runs | 1 per DAG | Prevents data corruption |
Security Considerations
| Layer | Control | Implementation |
|---|---|---|
| Authentication | IAM Execution Role | MWAA assumes role for all AWS calls |
| Authorization | IAM Policies | Least-privilege per service |
| Network | VPC Private Subnets | No public internet access |
| Secrets | Secrets Manager | SecretsManagerBackend for connections |
| Encryption at Rest | AES-256 | MWAA default encryption |
| Encryption in Transit | TLS 1.2+ | VPC endpoints + HTTPS |
| Audit | CloudTrail | All API calls logged |
| Logging | CloudWatch Logs | Scheduler, worker, web server logs |
Interview Questions & Answers
Q1: What is Amazon MWAA and how does it differ from running Airflow on EC2?
Answer: MWAA is a fully managed service that runs Apache Airflow on AWS. Unlike running Airflow on self-managed EC2 instances, MWAA handles infrastructure provisioning, Airflow version upgrades, high availability (dual schedulers, managed RDS metadata DB), auto-scaling workers, and VPC networking. You focus solely on writing DAGs. MWAA charges per environment hour rather than per EC2 instance, simplifying cost management. The trade-off is less customization (e.g., you cannot modify Airflow config beyond supported parameters) in exchange for zero operational overhead.
Q2: Explain the role of the Celery executor in MWAA.
Answer: MWAA uses the Celery executor to distribute task execution across multiple worker nodes. The scheduler submits tasks to an Amazon SQS queue (the Celery broker). Celery workers, running as ECS Fargate tasks, consume tasks from the queue and execute them. This architecture enables horizontal scaling ā as task volume increases, MWAA provisions additional workers up to the configured maximum. The SQS broker provides reliable, at-least-once delivery with dead-letter queue support for failed tasks.
Q3: How do you handle secrets and credentials in MWAA?
Answer: Secrets should be stored in AWS Secrets Manager or Systems Manager Parameter Store. MWAA integrates with SecretsManagerBackend to resolve Airflow Connections and Variables at runtime. Configure the backend in Airflow config: secrets_backend = airflow.providers.amazon.aws.secrets.secrets_manager.SecretsManagerBackend. Never hardcode credentials in DAG files. The MWAA execution role should have secretsmanager:GetSecretValue permission for the specific secret ARNs. Use separate secrets per environment (dev, staging, prod).
Q4: What is the difference between a Sensor and an Operator in Airflow?
Answer: An Operator executes a unit of work (e.g., GlueJobOperator launches a Glue job) and completes when the work is done. A Sensor polls an external system until a condition is met (e.g., S3KeySensor waits for a file to appear in S3). Sensors use poke_interval to check periodically. In reschedule mode, sensors release the worker slot between pokes, improving resource efficiency. Use sensors when your task depends on an external event rather than performing an action directly.
Q5: How would you design a DAG for a daily ETL pipeline?
Answer: The DAG should follow a linear dependency chain with TaskGroups for each phase: 1) Extract Group: S3KeySensor waits for raw data with timeout and reschedule mode, 2) Transform Group: GlueJobOperator launches the ETL job, 3) Load Group: RedshiftDataOperator executes a COPY command, 4) Validate Group: PythonOperator runs row count and null checks. Set max_active_runs=1, catchup=False, and retries=3 with exponential backoff. Use email_on_failure=True for alerting. Use XCom for passing metadata between tasks.
Q6: How does MWAA scale, and what are the constraints?
Answer: MWAA auto-scales Celery workers based on queued task volume. You configure min-workers (minimum 2 for HA) and max-workers (up to 20 per environment). The scheduler always runs as 2 replicas for fault tolerance. MWAA also supports scaling by creating a larger environment class (e.g., mw1.medium to mw1.large), which increases underlying compute resources. Key constraints: max 20 workers, max 500 DAGs per environment (recommended), and the metadata DB is single-AZ RDS.
Q7: What is XCom and when should you use it?
Answer: XCom (cross-communication) is Airflow's mechanism for passing small data payloads between tasks. Use it for metadata: row counts, file paths, status flags, configuration values. Do NOT use XCom for large datasets ā the payload is stored in the metadata database and has size limits. Use ti.xcom_push() in one task and ti.xcom_pull() in downstream tasks. For large data, pass S3 paths via XCom and have downstream tasks read directly from S3.
Q8: How do you optimize MWAA costs?
Answer: Cost optimization strategies: 1) Right-size workers based on average task concurrency, 2) Use reschedule mode for sensors to free worker slots, 3) Consolidate DAGs using TaskGroups instead of separate DAGs, 4) Set max_active_runs to prevent concurrent runs from consuming workers, 5) Schedule heavy ETL jobs during off-peak hours, 6) Start with mw1.small and scale up only when needed, 7) Shut down dev/test MWAA environments outside business hours using CLI automation, 8) Use spot-capable Fargate for batch workloads.
Common Pitfalls
| Pitfall | Consequence | Solution |
|---|---|---|
catchup=True on first deploy | Backfill storm creates thousands of runs | Always set catchup=False in production |
| Heavy imports at DAG level | Slow DAG parsing, high scheduler memory | Import inside functions/operators |
Sensors in poke mode | Worker slots held idle for hours | Use reschedule mode |
| Hardcoded credentials | Security risk, broken on rotation | Use Secrets Manager + Secret Scopes |
No execution_timeout | Runaway tasks consume workers indefinitely | Set timeout per operator |
| XCom for large data | Metadata DB bloat, slow queries | Pass S3 paths via XCom, read from S3 directly |
Missing email_on_failure | Silent failures, no alerting | Configure email + SNS integration |
No .airflowignore | Non-DAG files parsed every cycle | Exclude test files and configs |