Building a Scheduled AWS Lambda with Docker and DynamoDB: A Complete Guide

Building a Scheduled AWS Lambda with Docker and DynamoDB: A Complete Guide

Introduction

In this post, we’ll create a fully containerized AWS Lambda function that interacts with DynamoDB, triggered on a schedule by EventBridge. The function will:

  • Add a new item to DynamoDB and log all items every 5 minutes.
  • After 20 minutes (triggered by a separate EventBridge rule), clear all items from the table and log that it’s empty.

We’ll use:

  • AWS IAM: Create a user “Ted” to provision resources from AWS CloudShell.
  • DynamoDB: To store items.
  • ECR & Lambda: For containerizing and running the Node.js function.
  • EventBridge: To schedule and differentiate actions (one rule every 5 minutes, another every 20 minutes).
  • GitHub Actions with OIDC: For secure CI/CD with no long-lived credentials.

At the end, we’ll have a robust setup that dynamically changes its behavior based on which EventBridge rule triggered the Lambda function.

Prerequisites

  • An AWS account with administrative access.
  • A GitHub account.
  • Familiarity with AWS CLI, GitHub Actions, and basic Node.js development.

Region Choice:

We’ll use us-east-1 consistently. Ensure you select us-east-1 in the AWS Console when verifying or editing resources.

Step 1: Create the “Ted” IAM User

We are going to create a fictitious user in AWS Identity Account Manager (IAM) and do everything under the Ted user account to highlight assigning users in AWS. However, for simplicity, we are going to assign full admin access for Ted but in the real world you would practice assigning least privledges for best practices. That subject we'll cover in a future post.

1. Sign in to AWS Management Console as admin.

2. Go to IAM > Users > Add user:


  • Name: Ted
  • AWS Management Console & programmatic access: Enabled.
  • For simplicity, attach AdministratorAccess. (Use more restrictive policies in production.)




3. Save Ted’s credentials and log out. Log back in as Ted. Open AWS CloudShell (icon in the top right of AWS Console or open a browser in incognito mode).


For most of this exercize we will use AWS CloudShell.



Step 2: Set Up the Lambda Execution Role

cat > lambda-execution-trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

aws iam create-role \
  --role-name MyLambdaExecutionRole \
  --assume-role-policy-document file://lambda-execution-trust-policy.json \
  --region us-east-1

aws iam attach-role-policy \
  --role-name MyLambdaExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess \
  --region us-east-1

aws iam attach-role-policy \
  --role-name MyLambdaExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole \
  --region us-east-1        

Step 3: Create the DynamoDB Table

aws dynamodb create-table \
  --table-name MyDockerLambdaTable \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
  --region us-east-1        

Step 4: Create the ECR Repository

aws ecr create-repository \
  --repository-name my-docker-lambda \
  --region us-east-1        

Make a note of the repository URI returned.

Step 5: Set Up GitHub OIDC for CI/CD

Create OIDC Provider (if not done):

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --thumbprint-list xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  --client-id-list sts.amazonaws.com \
  --region us-east-1        

To obtain your --thumbprint-list, Run the following OpenSSL command in your terminal:

echo | openssl s_client -servername token.actions.githubusercontent.com -connect token.actions.githubusercontent.com:443 2>/dev/null | openssl x509 -fingerprint -noout -sha1        

Expected output:

SHA1 Fingerprint=69:38:FD:4D:98:BA:B0:3F:AA:DB:97:B3:43:96:83:1E:37:80:AE:A1        

Remove the colons (:) from the fingerprint to use it as --thumbprint-list:

6938fd4d98bab03faadb97b34396831e3780aea1        

Replaces the xxx's from the aws commands from earlier with your --thumbprint-list

Create Trust Policy for GitHub OIDC Role: Replace <ACCOUNT_ID> with your AWS Account ID and YourGitHubOrg/YourRepo with your repo:

cat > github-oidc-trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:sub": "repo:YourGitHubOrg/YourRepo:ref:refs/heads/main"
        }
      }
    }
  ]
}
EOF        
aws iam create-role \
  --role-name YourGitHubOIDCDeployRole \
  --assume-role-policy-document file://github-oidc-trust-policy.json \
  --region us-east-1        

Attach a Policy for ECR & Lambda Updates:

cat > github-oidc-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage",
        "lambda:UpdateFunctionCode"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::<ACCOUNT_ID>:role/MyLambdaExecutionRole"
    }
  ]
}
EOF        
aws iam put-role-policy \
  --role-name YourGitHubOIDCDeployRole \
  --policy-name GitHubOIDCPolicy \
  --policy-document file://github-oidc-policy.json \
  --region us-east-1        

Step 6: The Lambda Code and Dockerfile

We’ll modify the code to determine its action (add or clear) based on the EventBridge rule ARN in event.resources. The 5-minute rule triggers “add,” the 20-minute rule triggers “clear.”

Update <REGION> and <ACCOUNT_ID> before use.

index.js:

const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || 'MyDockerLambdaTable';

// Replace with your actual region and account ID
const REGION = 'us-east-1';
const ACCOUNT_ID = '<ACCOUNT_ID>';

const FIVE_MINUTE_RULE_ARN = `arn:aws:events:${REGION}:${ACCOUNT_ID}:rule/MyFiveMinuteRule`;
const TWENTY_MINUTE_RULE_ARN = `arn:aws:events:${REGION}:${ACCOUNT_ID}:rule/MyTwentyMinuteRule`;

exports.handler = async (event) => {
    console.log('Received event:', JSON.stringify(event, null, 2));

    let action = 'add'; // default action
    if (Array.isArray(event.resources)) {
        if (event.resources.includes(TWENTY_MINUTE_RULE_ARN)) {
            action = 'clear';
        } else if (event.resources.includes(FIVE_MINUTE_RULE_ARN)) {
            action = 'add';
        }
    }

    console.log(`Action determined: ${action}`);

    if (action === 'add') {
        const now = Date.now();
        const newItem = { id: `item-${now}`, timestamp: now };
        console.log('Adding a new item:', newItem);

        await dynamo.put({ TableName: TABLE_NAME, Item: newItem }).promise();
        console.log('Item added successfully.');

        const data = await dynamo.scan({ TableName: TABLE_NAME }).promise();
        console.log('Current items in DynamoDB:', data.Items || []);

        return { statusCode: 200, body: 'Items added/read successfully.' };

    } else if (action === 'clear') {
        console.log('Clearing table...');
        const data = await dynamo.scan({ TableName: TABLE_NAME }).promise();
        const items = data.Items || [];
        console.log(`Found ${items.length} items to delete.`);

        for (let item of items) {
            await dynamo.delete({ TableName: TABLE_NAME, Key: { id: item.id } }).promise();
        }

        console.log('Table cleared. Now empty.');
        return { statusCode: 200, body: 'Table cleared.' };

    } else {
        console.error(`Unknown action: ${action}`);
        return { statusCode: 400, body: 'Unknown action.' };
    }
};        

package.json:

{
  "name": "iamlambda",
  "version": "1.0.0",
  "dependencies": {
    "aws-sdk": "^2.1000.0"
  }
}        

Dockerfile:

FROM public.ecr.aws/lambda/nodejs:18

# Copy code
COPY index.js package*.json ./

# Install dependencies
RUN npm install

CMD [ "index.handler" ]        

Push these files to your GitHub repository’s main branch.

Step 7: Create the Lambda Function Initially

aws lambda create-function \
  --function-name MyDockerLambdaFunction \
  --package-type Image \
  --code ImageUri=<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/my-docker-lambda:latest \
  --role arn:aws:iam::<ACCOUNT_ID>:role/MyLambdaExecutionRole \
  --environment Variables="{TABLE_NAME=MyDockerLambdaTable}" \
  --region us-east-1        

If the image isn’t pushed yet, run the GitHub Actions workflow after setting it up, then re-run this command.

Step 8: GitHub Actions Workflow

Create .github/workflows/deploy.yml:

name: Deploy Lambda

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/YourGitHubOIDCDeployRole
          aws-region: us-east-1

      - name: Build Docker image
        run: docker build -t my-docker-lambda:latest .

      - name: Login to ECR
        run: |
          aws ecr get-login-password --region us-east-1 \
          | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com

      - name: Tag Docker image
        run: docker tag my-docker-lambda:latest <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/my-docker-lambda:latest

      - name: Push Docker image
        run: docker push <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/my-docker-lambda:latest

      - name: Update Lambda to use new container image
        run: |
          aws lambda update-function-code \
            --function-name MyDockerLambdaFunction \
            --image-uri <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/my-docker-lambda:latest        

Push this to main. The workflow will build, push the image, and update the Lambda.

Step 9: Set Up EventBridge Rules in AWS Console

We need two rules:

MyFiveMinuteRule (every 5 minutes)

  • EventBridge > Rules > Create rule.
  • Name: MyFiveMinuteRule.
  • Schedule pattern: cron(0/5 * * * ? *) (every 5 minutes).
  • Target: MyDockerLambdaFunction.
  • Save the rule.

This rule’s ARN will match our code and result in action = add.

Select Add Trigger


Select EventBridge (CloudWatch Events)


Enter the Rule name and Scheduled Expression followed by Add

MyTwentyMinuteRule (every 20 minutes) Repeat the process just like the 5 minute rule:

  • Create another rule:
  • Name: MyTwentyMinuteRule.
  • Schedule pattern: cron(0/20 * * * ? *) (every 20 minutes).
  • Target: MyDockerLambdaFunction.
  • Save the rule.

This rule’s ARN will cause action = clear in the code.

No input transformers are needed here since we’re distinguishing based on resources array in the event.

Step 10: Verification

  • After a few invocations (5-minute rule), check CloudWatch Logs for MyDockerLambdaFunction.
  • Items should be added and listed every 5 minutes.
  • At the 20-minute interval, when MyTwentyMinuteRule fires, the code should detect the TWENTY_MINUTE_RULE_ARN in event.resources and clear the table.
  • Logs should show “Table cleared. Now empty.”

Check DynamoDB table to see the cycle of adding and clearing.

Step 11: Cleanup

When finished:

  1. Remove EventBridge targets and rules from the console.
  2. Delete the Lambda:

aws lambda delete-function --function-name MyDockerLambdaFunction --region us-east-1        

Delete the DynamoDB table:

aws dynamodb delete-table --table-name MyDockerLambdaTable --region us-east-1        

Delete the ECR repository:

aws ecr delete-repository --repository-name my-docker-lambda --force --region us-east-1        

Detach policies and delete IAM roles:

aws iam detach-role-policy --role-name MyLambdaExecutionRole --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
aws iam detach-role-policy --role-name MyLambdaExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name MyLambdaExecutionRole

aws iam delete-role-policy --role-name YourGitHubOIDCDeployRole --policy-name GitHubOIDCPolicy || true
aws iam delete-role --role-name YourGitHubOIDCDeployRole        

Go back into your own accuont and delete Ted’s user (remove login profile and policies first if any):

aws iam delete-login-profile --user-name Ted
aws iam delete-user --user-name Ted        

If the above commands do not work, you will have to go into your main aws account and manually delete Ted's policies and then delete the Ted User from your users in IAM.

Conclusion

In this final, working solution, we:

  • Created an IAM user (Ted) and set up all necessary AWS resources via CloudShell.
  • Leveraged GitHub OIDC for secure CI/CD, building and updating a Dockerized Lambda.
  • Created two EventBridge rules for scheduling: one for every 5 minutes, one for every 20 minutes.
  • Adjusted the Lambda code to determine the action (add or clear) based on which EventBridge rule’s ARN triggered the event, ensuring correct behavior without extra input transformers.
  • Verified the Lambda adds items regularly and clears them after the 20-minute rule fires.

By following this guide, you have a robust, scheduled, containerized Lambda solution that integrates DynamoDB, AWS CI/CD best practices, and dynamic behavior based on EventBridge triggers.

要查看或添加评论,请登录

Erik Robles的更多文章

社区洞察

其他会员也浏览了