šŸŽ‰ 75% of content is free forever — Unlock Premium from $10/mo →
CW
šŸ’¼ Servicesā„¹ļø Aboutāœ‰ļø ContactView Pricing Plansfrom $10

AWS Managed Airflow for Data Engineers

AWS Data EngineeringMWAA - Managed Workflows for Apache Airflow⭐ Premium

Advertisement

Amazon MWAA for Data Engineers

Master Amazon Managed Workflows for Apache Airflow — build, schedule, and monitor complex data pipelines using DAGs, operators, and sensor-driven orchestration on AWS.

20 min readIntermediate

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

Amazon MWAA Architecture on AWSAWS VPC (Private Subnets)MWAA EnvironmentScheduler (x2)HA dual replicasWeb ServerAirflow UIMetadata DBRDS PostgreSQLCelery Workers (ECS Fargate)Auto-scale: 2-20 workersSQS BrokerTask queue + DLQS3 - DAG Storages3://airflow-dags/dags/ | plugins/ | config/Sync interval: 5 min (configurable)Secrets ManagerConnections + VariablesSecretsManagerBackendAWS GlueETL Jobs + CrawlersAmazon RedshiftData WarehouseAWS LambdaServerless FunctionsAmazon EMRSpark / HadoopCloudWatch Logs + Metrics + AlarmsScheduler heartbeat | Failed tasks | Worker count | DAG runs

Real-World Project Structure

A production MWAA deployment follows a disciplined directory and pipeline structure:

Architecture Diagram
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

CriteriaMWAAStep FunctionsGlue Workflows
ComplexityHigh (Python DAGs)Medium (JSON/YAML)Low (visual)
ServicesAny AWS + externalAWS-native onlyGlue + limited
Cost ModelAlways-on (per hour)Pay-per-executionPer DPU-hour
ScalingWorker auto-scalingAutomaticAutomatic
Error HandlingRetry, callbacks, SLAsBuilt-in catch/retryBasic
MonitoringAirflow UI + CloudWatchConsole + CloudWatchGlue Console
Learning CurveSteep (Airflow concepts)ModerateLow
Community800+ operatorsAWS-onlyNone
Best ForComplex branching, cross-serviceLambda-heavy, event-drivenGlue ETL orchestration

Performance Considerations

MetricRecommendedImpact
Workers (min)2 (HA minimum)Fault tolerance
Workers (max)Based on peak queueCost vs. throughput
Scheduler Heartbeat5 seconds (default)Task submission frequency
DAG Sync Interval5 minutes (default)Deployment latency
Sensor ModerescheduleFrees worker slots between pokes
Task Retries2-3 with exponential backoffTransient failure recovery
Execution TimeoutSet per operatorPrevents runaway tasks
max_active_runs1 per DAGPrevents data corruption

Security Considerations

LayerControlImplementation
AuthenticationIAM Execution RoleMWAA assumes role for all AWS calls
AuthorizationIAM PoliciesLeast-privilege per service
NetworkVPC Private SubnetsNo public internet access
SecretsSecrets ManagerSecretsManagerBackend for connections
Encryption at RestAES-256MWAA default encryption
Encryption in TransitTLS 1.2+VPC endpoints + HTTPS
AuditCloudTrailAll API calls logged
LoggingCloudWatch LogsScheduler, 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

PitfallConsequenceSolution
catchup=True on first deployBackfill storm creates thousands of runsAlways set catchup=False in production
Heavy imports at DAG levelSlow DAG parsing, high scheduler memoryImport inside functions/operators
Sensors in poke modeWorker slots held idle for hoursUse reschedule mode
Hardcoded credentialsSecurity risk, broken on rotationUse Secrets Manager + Secret Scopes
No execution_timeoutRunaway tasks consume workers indefinitelySet timeout per operator
XCom for large dataMetadata DB bloat, slow queriesPass S3 paths via XCom, read from S3 directly
Missing email_on_failureSilent failures, no alertingConfigure email + SNS integration
No .airflowignoreNon-DAG files parsed every cycleExclude test files and configs

QuizBox

See Also

šŸ”’

Premium Content

AWS Managed Airflow for Data Engineers

You've previewed the first section. Unlock this full lesson and 900+ advanced tutorials with a Premium plan.

šŸŽÆEnd-to-end Projects
šŸ’¼Interview Prep
šŸ“œCertificates
šŸ¤Community Access

Already a member? Log in

Advertisement