devuplabs.cloud
PreviewLab4 hours

Scheduler Lab

Create one-time, rate, and cron schedules with EventBridge Scheduler, timezones, flexible windows, and DLQs.

Mental model

EventBridge Scheduler is a serverless time trigger. At time T, it calls a target. Think of it as a cron daemon in the cloud, but with IAM, retry logic, DLQs, and the ability to invoke any AWS service or external API.

Prerequisites

0 of 5 checked

IAM Roles for EventBridge Scheduler

Goal

Understand the two-role model that Scheduler requires. Build both roles correctly. Observe what breaks when they're swapped or misconfigured.

Estimated time: 45 min


The two-role model

What's happening here

EventBridge Scheduler requires two completely separate IAM roles, each with a different trust principal and a different job. Confusing them is the #1 source of silent failures.

  • Scheduler Execution Role: assumed by the *Scheduler service itself* to call your target (Lambda, SQS, ECS, API Gateway, etc.). Trust principal: scheduler.amazonaws.com. This role needs permission to invoke the target.
  • Lambda Execution Role: assumed by *Lambda* when it runs your function. Trust principal: lambda.amazonaws.com. This role needs permission to write CloudWatch logs, access DynamoDB, call S3, and so on.

The Scheduler does NOT assume the Lambda role. Lambda does NOT assume the Scheduler role. They are independent. Getting this wrong causes AccessDenied errors that are hard to trace because the error surfaces in Scheduler's internal logs, not Lambda's.

javascript
EventBridge Scheduler
  │
  │── assumes ──▶  Scheduler Execution Role (trust: scheduler.amazonaws.com)
  │                  └── Permission: lambda:InvokeFunction on target Lambda
  │
  └── invokes ──▶  Lambda Function
                     │
                     └── assumes ──▶  Lambda Execution Role (trust: lambda.amazonaws.com)
                                        └── Permission: logs:*, dynamodb:*, s3:*, etc.

Create the Scheduler Execution Role

What's happening here

The Scheduler Execution Role's trust policy allows scheduler.amazonaws.com to call sts:AssumeRole on it. Without this, the Scheduler service can't assume the role and all schedule invocations fail silently. The inline policy on this role grants lambda:InvokeFunction. This is the permission Scheduler uses *after* it assumes the role, it calls the Lambda invoke API on your behalf.

bash
# 1. Trust policy: allows Scheduler service to assume this role
cat > scheduler-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "scheduler.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF

# 2. Create the Scheduler Execution Role
aws iam create-role \
  --role-name eventbridge-scheduler-exec-role \
  --assume-role-policy-document file://scheduler-trust-policy.json \
  --description "Assumed by EventBridge Scheduler to invoke targets"

export SCHEDULER_ROLE_ARN=$(aws iam get-role \
  --role-name eventbridge-scheduler-exec-role \
  --query 'Role.Arn' --output text)

echo "Scheduler Role ARN: $SCHEDULER_ROLE_ARN"

# 3. Inline policy: allow Scheduler to invoke any Lambda in this account
aws iam put-role-policy \
  --role-name eventbridge-scheduler-exec-role \
  --policy-name InvokeLambda \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:*:*:function:*"
    }]
  }'
  
aws iam get-role --role-name eventbridge-scheduler-exec-role --query 'Role.AssumeRolePolicyDocument'
# trust principal must be 'scheduler.amazonaws.com'

Create the Lambda Execution Role

What's happening here

Lambda needs its own role to write logs to CloudWatch. The trust principal here is lambda.amazonaws.com: completely different from the Scheduler role. AWSLambdaBasicExecutionRole is a managed policy that grants logs:CreateLogGroup, logs:CreateLogStream, and logs:PutLogEvents: the minimum for Lambda to function. If you attach AWSLambdaBasicExecutionRole to the Scheduler role instead of the Lambda role, Lambda will run but can't write logs. Debugging becomes blind.

bash
# 1. Trust policy: allows Lambda service to assume this role
cat > lambda-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF

# 2. Create Lambda Execution Role
aws iam create-role \
  --role-name lambda-scheduler-lab-role \
  --assume-role-policy-document file://lambda-trust-policy.json \
  --description "Assumed by Lambda functions in the Scheduler lab"

export LAMBDA_ROLE_ARN=$(aws iam get-role \
  --role-name lambda-scheduler-lab-role \
  --query 'Role.Arn' --output text)

echo "Lambda Role ARN: $LAMBDA_ROLE_ARN"

# 3. Attach managed policy for CloudWatch Logs
aws iam attach-role-policy \
  --role-name lambda-scheduler-lab-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

sleep 10  # IAM propagation

Checkpoint: both roles exist, with different trust principals. Run both and compare:

bash
aws iam get-role --role-name eventbridge-scheduler-exec-role --query 'Role.AssumeRolePolicyDocument.Statement[0].Principal'
aws iam get-role --role-name lambda-scheduler-lab-role --query 'Role.AssumeRolePolicyDocument.Statement[0].Principal'

Break It: Session 0

Break 1: Swap the trust principals

bash
# Create a role with lambda.amazonaws.com trust, then try using it as Scheduler's role
# The Scheduler can't assume it → invocation silently fails
# Observe: no Lambda execution in CloudWatch Logs, no error thrown to you
aws iam get-role \
  --role-name lambda-scheduler-lab-role \
  --query 'Role.AssumeRolePolicyDocument.Statement[0].Principal'
# Output: {"Service": "lambda.amazonaws.com"}
# If you pass this ARN to a schedule's RoleArn, Scheduler can't assume it → silent failure

Scheduler won't throw an error at schedule creation time. The failure happens silently at invocation time. You'll see InvocationAttemptCount increment in CloudWatch metrics but Lambda never runs. This is the hardest class of Scheduler bugs to debug.

Break 2: Missing lambda:InvokeFunction permission

bash
# Remove the InvokeLambda policy and observe what happens
aws iam delete-role-policy \
  --role-name eventbridge-scheduler-exec-role \
  --policy-name InvokeLambda

# Create a test schedule (we'll build a real Lambda in Session 1)
# After the schedule fires, check CloudWatch Scheduler metrics:
aws cloudwatch get-metric-statistics \
  --namespace AWS/Scheduler \
  --metric-name InvocationDroppedCount \
  --start-time $(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-10M +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 300 --statistics Sum \
  --region $AWS_REGION

# Re-add the policy before continuing
aws iam put-role-policy \
  --role-name eventbridge-scheduler-exec-role \
  --policy-name InvokeLambda \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:*:*:function:*"
    }]
  }'

InvocationDroppedCount goes up, not FailedInvocations. Dropped = Scheduler couldn't even call the target API. Failed = target was called but errored. Different metrics, different root causes.

Break 3: No CloudWatch policy on the Lambda role

bash
# Detach AWSLambdaBasicExecutionRole from the Lambda role
aws iam detach-role-policy \
  --role-name lambda-scheduler-lab-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# Now create a Lambda function and invoke it manually
# Lambda runs but produces no CloudWatch logs at all
# Re-attach before continuing:
aws iam attach-role-policy \
  --role-name lambda-scheduler-lab-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Lambda returns a 200 response (it ran), but /aws/lambda/<function-name> log group has no streams. Debugging scheduled functions without logs is nearly impossible in production.


Fundamentals & First Schedule

Goal

Understand Scheduler core concepts. Create cron and rate-based schedules via CLI. Verify execution through CloudWatch Logs.

Estimated time: 60 min


Scheduler vs. EventBridge Event Bus vs. CloudWatch Events

What's happening here

Three different things are often conflated under "EventBridge":

  • EventBridge Event Bus: reactive pub/sub. An event happens → rule matches → target invoked. You don't control *when*, only *what*.
  • EventBridge Scheduler: proactive time-driven. At time T → invoke target. No event, no bus, no rule needed. This is what this lab covers.
  • CloudWatch Events: the legacy predecessor. Still works. Weaker timezone support, fewer targets, no flexible time windows. Scheduler is the replacement.

Schedule expression types:

  • rate(N unit): fires every N units from creation time. rate(5 minutes), rate(1 hour), rate(7 days).
  • cron(m h dom mon dow yr), 6-field cron with year. cron(0 9 ? * MON-FRI *) = 9 AM every weekday.
  • at(yyyy-mm-ddThh:mm:ss): one-time schedule at a specific UTC datetime.

Scheduler invokes targets asynchronously by default for Lambda. It doesn't wait for a response. Errors only surface in Lambda's async metrics or your DLQ.


Create your first Lambda function

bash
mkdir -p /tmp/scheduler-lab

cat > /tmp/scheduler-lab/index.mjs << 'EOF'
export const handler = async (event) => {
  const ts = new Date().toISOString();
  console.log(JSON.stringify({
    invoked_at: ts,
    schedule_payload: event
  }));
  return { status: "ok", invoked_at: ts };
};
EOF

cd /tmp/scheduler-lab && zip function.zip index.mjs

aws lambda create-function \
  --function-name scheduler-lab-fn \
  --runtime nodejs20.x \
  --handler index.handler \
  --role $LAMBDA_ROLE_ARN \
  --zip-file fileb://function.zip \
  --region $AWS_REGION

export LAMBDA_ARN=$(aws lambda get-function \
  --function-name scheduler-lab-fn \
  --region $AWS_REGION \
  --query 'Configuration.FunctionArn' --output text)

echo "Lambda ARN: $LAMBDA_ARN"

Checkpoint: aws lambda get-function --function-name scheduler-lab-fn --region $AWS_REGION --query 'Configuration.State'Active.


Create a rate-based schedule

What's happening here

A rate schedule fires on a fixed interval. rate(5 minutes) means "every 5 minutes from now". The interval is measured from the schedule's creation time, not from a wall-clock boundary. --flexible-time-window '{"Mode": "OFF"}' means fire at exactly the scheduled time. We'll explore flexible windows in Session 4. The --target JSON tells Scheduler what to call and with what payload. Input is a JSON string injected as the Lambda event, use it to pass schedule name, tenant ID, or any context your function needs.

Unlock all 24 AWS services & 291+ lab sessions (~180 hours)

Pricing