Event-Driven Transaction Router
Route banking transactions with EventBridge rules and persist results in RDS MySQL.
Introduction
30 Second Summary
Every time you tap your phone to pay for coffee, transfer money to a friend, or deposit a paycheck, your bank routes that transaction to the right handler behind the scenes. Different transaction types need different processing logic, and the system needs to figure out where each one goes without anyone manually sorting them.
In this project, you will build a banking transaction processing pipeline that automatically routes events to different processors based on their type. You will publish transaction events, watch them get intelligently routed, and verify the results in a real database.
What You'll Build
An event-driven transaction routing system using Amazon EventBridge for content-based routing and RDS MySQL for persisting processed results.
By the end of this project, you'll have:
- A custom EventBridge event bus with three content-based routing rules that match on transaction type.
- Three Lambda functions deployed in a VPC that each process a specific transaction type and write results to a database.
- An RDS MySQL instance with a transactions table proving that each event was routed to the correct processor.
- Secret Mission: Add an EventBridge archive to capture all events and replay a past transaction back through the pipeline.
Are there any prerequisites?
You will need an AWS account with the AWS CLI configured, basic Python knowledge, and VS Code installed. Familiarity with running terminal commands is helpful.
Set Up Infrastructure
Before you can route banking transactions through EventBridge, you need the foundational AWS resources that your pipeline depends on.
In this step, you will create the networking and permissions layer that connects Lambda functions to an RDS MySQL database. This includes security groups that control network access, an IAM role that grants Lambda the right permissions, and the database instance itself.
In this step, get ready to:
- Create security groups for Lambda and RDS with proper network rules.
- Create an IAM role with the managed policies Lambda needs to run in a VPC.
- Launch an RDS MySQL instance that will store processed transactions.
Create security groups for Lambda and RDS
Security groups act as virtual firewalls that control which traffic can reach your AWS resources. You need two: one for Lambda (which makes outbound connections to RDS) and one for RDS (which only accepts inbound connections from Lambda on port 3306).
- Identify your default VPC ID by running this command:
aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query "Vpcs[0].VpcId" --output text
What does this command do?
This queries your AWS account for the default VPC and returns just its ID. You will use this VPC ID in the next commands to create security groups in the right network.
- Copy the VPC ID from the output (it looks like vpc-0abc123def456). You will use it in the next commands.
- Retrieve your subnet IDs by running this command:
aws ec2 describe-subnets --filters Name=vpc-id,Values=[[VPC_ID="your VPC ID from above"]] --query "Subnets[*].SubnetId" --output text
Why do you need subnet IDs?
Lambda functions running inside a VPC need to be placed in specific subnets. You will pass these subnet IDs when creating Lambda functions in the next step.
- Copy and save at least two subnet IDs from the output. They look like subnet-0abc123.
- Create the Lambda security group by running this command:
aws ec2 create-security-group --group-name lambda-sg --description "Security group for Lambda functions" --vpc-id [[VPC_ID="your VPC ID"]]
What does this command do?
This creates a security group named lambda-sg in your default VPC. Lambda does not need inbound rules because it only makes outbound connections to RDS.
- Copy the GroupId from the output (it looks like sg-0abc123). This is your Lambda security group ID.
- Create the RDS security group by running this command:
aws ec2 create-security-group --group-name rds-sg --description "Security group for RDS MySQL" --vpc-id [[VPC_ID="your VPC ID"]]
- Copy the GroupId from the output. This is your RDS security group ID.
- Add an inbound rule to the RDS security group that allows the Lambda security group to connect on port 3306 by running this command:
aws ec2 authorize-security-group-ingress --group-id [[RDS_SG_ID="your RDS security group ID"]] --protocol tcp --port 3306 --source-group [[LAMBDA_SG_ID="your Lambda security group ID"]]
What does this rule do?
This tells AWS that only resources attached to lambda-sg can connect to resources attached to rds-sg on port 3306 (the MySQL port). This is the standard security pattern for Lambda-to-RDS connectivity.
- You should see a response containing "Return": true in the output, confirming the rule was added.
Getting an InvalidGroup error?
Make sure the security group IDs you pasted start with sg- and match the output from the previous create commands. Double-check that you are using the RDS group ID as --group-id and the Lambda group ID as --source-group.
Help me fix my security group ingress rule
Create an IAM role for Lambda
Lambda functions need an execution role that grants them permission to run inside a VPC and write logs. You will create this role with a trust policy that allows the Lambda service to assume it, then attach the required managed policies.
- Create the IAM role with a trust policy for Lambda by running this command:
aws iam create-role --role-name lambda-rds-role --assume-role-policy-document "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
What is a trust policy?
A trust policy tells AWS which services are allowed to assume (use) this role. Here you are telling AWS that the Lambda service can use this role when running your functions.
- Copy the Arn value from the output. It looks like arn:aws:iam::123456789012:role/lambda-rds-role. Save this for the next step.
- Attach the VPC access policy so Lambda can create network interfaces inside your VPC:
aws iam attach-role-policy --role-name lambda-rds-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
- Attach the basic execution policy so Lambda can write to CloudWatch Logs:
aws iam attach-role-policy --role-name lambda-rds-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
What do these policies grant?
AWSLambdaVPCAccessExecutionRole grants permissions to create and manage network interfaces in your VPC subnets. This is required for any Lambda function that runs inside a VPC.
AWSLambdaBasicExecutionRole grants permissions to write logs to CloudWatch. You will use these logs later to verify that your Lambda functions processed events correctly.
- Verify the policies are attached by running this command:
aws iam list-attached-role-policies --role-name lambda-rds-role
- You should see both AWSLambdaVPCAccessExecutionRole and AWSLambdaBasicExecutionRole listed in the output.
Seeing an access denied error?
Your AWS CLI user needs IAM permissions to create roles and attach policies. Make sure your CLI credentials belong to a user or role with the IAMFullAccess managed policy or equivalent permissions.
Help me fix my IAM permissions error
Launch the RDS MySQL instance
The RDS instance is your pipeline's destination database. It takes 5-10 minutes to provision, so you will start it now and continue working while it creates.
Cost warning
RDS db.t3.micro costs approximately $0.017/hour outside the AWS Free Tier. Tear down all resources within 1-2 hours of completing this project to keep costs under $0.10.
- Launch the RDS MySQL instance by running this command:
aws rds create-db-instance --db-instance-identifier banking-db --db-instance-class db.t3.micro --engine mysql --master-username admin --master-user-password [[DB_PASSWORD="a strong password (8+ characters, letters and numbers)"]] --allocated-storage 20 --vpc-security-group-ids [[RDS_SG_ID="your RDS security group ID"]]
What does this command do?
- --db-instance-identifier banking-db names your database instance.
- --db-instance-class db.t3.micro sets the instance size (the smallest available).
- --engine mysql tells RDS to run MySQL.
- --allocated-storage 20 provisions 20 GB of storage.
- --vpc-security-group-ids attaches your RDS security group so only Lambda can connect.
Remember your password
Save the password you chose somewhere safe. You will need it when connecting to the database later and when configuring your Lambda functions.
- The instance takes 5-10 minutes to provision. Check its status by running this command:
aws rds describe-db-instances --db-instance-identifier banking-db --query "DBInstances[0].DBInstanceStatus" --output text
- You should see creating as the status. You can continue to the next step while it provisions.
- If you want to wait until it is ready, run this command (it blocks until the instance is available):
aws rds wait db-instance-available --db-instance-identifier banking-db
- Once the instance is available, retrieve the endpoint address by running this command:
aws rds describe-db-instances --db-instance-identifier banking-db --query "DBInstances[0].Endpoint.Address" --output text
- Copy the endpoint address from the output. It looks like banking-db.abc123.us-east-1.rds.amazonaws.com. Save this for later.
RDS creation failed?
If you see an error about the security group, double-check that the --vpc-security-group-ids value is the RDS security group ID (not the Lambda one). The ID should start with sg-.
If you see a password policy error, ensure your password is at least 8 characters and contains letters and numbers.
Help me fix my RDS creation error
Your infrastructure foundation is in place. Next up, you will write the Lambda function code that processes banking transactions and deploy it to AWS.
Create and Deploy Lambda Functions
Your infrastructure is taking shape. The security groups, IAM role, and RDS instance from the previous step form the networking and data layer your Lambda functions will connect to.
Now it's time to write the code that actually processes transactions. You'll create a single Python handler that works for all three transaction types, differentiated only by an environment variable. Then you'll package it with its database dependency and deploy three separate functions to AWS.
In this step, get ready to:
- Write a shared Lambda handler that connects to RDS and inserts transaction records.
- Package the handler with the pymysql dependency into a deployment zip.
- Deploy three Lambda functions with VPC configuration and unique environment variables.
Write the shared Lambda handler
Instead of writing three separate Python files, you'll write one handler and deploy it three times. Each deployment gets a different PROCESSOR_NAME environment variable so it stamps the correct processor identity when it writes to the database.
Why one codebase for three functions?
All three transaction types follow the same logic: receive an event, extract fields, insert into the database. The only difference is which processor name gets recorded. A single file deployed three times means you fix bugs in one place and reduce maintenance overhead.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir banking-lambda
cd banking-lambda
- Confirm the folder was created by running:
pwd
You should see a path ending in banking-lambda (for example /Users/yourname/Desktop/banking-lambda).
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the banking-lambda folder on your Desktop, then click Open.
- Create a new file called lambda_function.py by clicking the new file icon in the Explorer sidebar.
Start by adding the imports and configuration that read from environment variables.
- Copy the following code into lambda_function.py:
import json
import os
import pymysql
import logging
from datetime import datetime
logger = logging.getLogger()
logger.setLevel(logging.INFO)
db_host = os.environ['DB_HOST']
db_user = os.environ['DB_USER']
db_password = os.environ['DB_PASSWORD']
db_name = os.environ['DB_NAME']
processor_name = os.environ['PROCESSOR_NAME']
What does this code do?
- The imports bring in pymysql for MySQL connections and standard libraries for JSON handling, logging, and timestamps.
- The five os.environ calls read database connection details and the processor name from environment variables set during deployment.
- Each Lambda function deployment will have different values for these variables, which is how one codebase becomes three distinct processors.
- Below the environment variables, add the database connection function:
def get_connection():
return pymysql.connect(
host=db_host,
user=db_user,
passwd=db_password,
db=db_name,
connect_timeout=5
)
What does this code do?
The get_connection() function creates a connection to the RDS MySQL database using the environment variables loaded at module level. The connect_timeout=5 prevents the function from hanging if the database is unreachable.
- Below get_connection(), add the main handler function:
def lambda_handler(event, context):
logger.info(f"Received event: {json.dumps(event)}")
detail = event.get('detail', {})
transaction_id = detail.get('transaction_id', 'unknown')
transaction_type = detail.get('transaction_type', 'unknown')
account_number = detail.get('account_number', 'unknown')
amount = detail.get('amount', 0)
try:
conn = get_connection()
with conn.cursor() as cursor:
sql = """INSERT INTO transactions (transaction_id, transaction_type, account_number, amount, processor, processed_at)
VALUES (%s, %s, %s, %s, %s, %s)"""
cursor.execute(sql, (transaction_id, transaction_type, account_number, amount, processor_name, datetime.utcnow()))
conn.commit()
conn.close()
logger.info(f"Transaction {transaction_id} processed by {processor_name}")
return {
'statusCode': 200,
'body': json.dumps({
'message': f'Transaction {transaction_id} processed by {processor_name}',
'transaction_type': transaction_type,
'account_number': account_number,
'amount': str(amount)
})
}
except Exception as e:
logger.error(f"Error processing transaction: {str(e)}")
raise e
What does this code do?
- The lambda_handler function is the entry point that EventBridge invokes when a matching event arrives.
- It extracts transaction details (ID, type, account, amount) from the event's detail field.
- It opens a database connection, inserts a row into the transactions table with the processor's identity stamp, then commits and closes.
- If anything fails, the error is logged and re-raised so Lambda marks the invocation as failed.
- Save lambda_function.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import json
import os
import pymysql
import logging
from datetime import datetime
logger = logging.getLogger()
logger.setLevel(logging.INFO)
db_host = os.environ['DB_HOST']
db_user = os.environ['DB_USER']
db_password = os.environ['DB_PASSWORD']
db_name = os.environ['DB_NAME']
processor_name = os.environ['PROCESSOR_NAME']
def get_connection():
return pymysql.connect(
host=db_host,
user=db_user,
passwd=db_password,
db=db_name,
connect_timeout=5
)
def lambda_handler(event, context):
logger.info(f"Received event: {json.dumps(event)}")
detail = event.get('detail', {})
transaction_id = detail.get('transaction_id', 'unknown')
transaction_type = detail.get('transaction_type', 'unknown')
account_number = detail.get('account_number', 'unknown')
amount = detail.get('amount', 0)
try:
conn = get_connection()
with conn.cursor() as cursor:
sql = """INSERT INTO transactions (transaction_id, transaction_type, account_number, amount, processor, processed_at)
VALUES (%s, %s, %s, %s, %s, %s)"""
cursor.execute(sql, (transaction_id, transaction_type, account_number, amount, processor_name, datetime.utcnow()))
conn.commit()
conn.close()
logger.info(f"Transaction {transaction_id} processed by {processor_name}")
return {
'statusCode': 200,
'body': json.dumps({
'message': f'Transaction {transaction_id} processed by {processor_name}',
'transaction_type': transaction_type,
'account_number': account_number,
'amount': str(amount)
})
}
except Exception as e:
logger.error(f"Error processing transaction: {str(e)}")
raise e
Package and deploy the Lambda functions
Lambda needs all dependencies bundled into a single zip file. Since pymysql is a third-party library (not included in the Lambda runtime), you'll install it directly into your project folder and zip everything together.
- Switch back to your terminal. Make sure you are inside the banking-lambda directory.
- Install pymysql into the current directory by running:
pip install pymysql -t .
What does the -t flag do?
The -t . flag tells pip to install the package into the current directory instead of your global Python environment. This puts the library files alongside your handler so Lambda can find them at runtime.
- Create the deployment zip package by running:
macOS
zip -r lambda.zip .
Windows
Use Compress-Archive in PowerShell to create a Lambda-compatible zip:
Compress-Archive -Path * -DestinationPath lambda.zip
- Confirm lambda.zip was created by running:
ls
You should see lambda.zip and lambda_function.py in the output alongside the pymysql package files.
Don't see lambda.zip?
- Make sure you're in the banking-lambda directory. Run pwd (macOS) or cd (Windows) to check your current location.
- On Windows, if you used a different zip method and the file structure looks wrong, try using Git Bash with the zip -r lambda.zip . command instead.
Help me troubleshoot my Lambda zip packaging.
Before deploying, you need the RDS endpoint address for the database connection. Your RDS instance from Step 1 needs to be available.
- Check that your RDS instance is ready and retrieve its endpoint by running:
aws rds describe-db-instances --db-instance-identifier banking-db --query "DBInstances[0].Endpoint.Address" --output text
What should I see?
You should see a hostname like banking-db.xxxxxxxxxx.us-east-1.rds.amazonaws.com. Copy this value. You'll use it as the DB_HOST environment variable in the next commands.
Seeing 'None' or an empty result?
Your RDS instance is still provisioning. Run the wait command to pause until it's ready:
aws rds wait db-instance-available --db-instance-identifier banking-db
This can take 5-10 minutes. Once it completes, re-run the describe command above.
Help me check my RDS instance status.
Now you'll deploy three Lambda functions, each with the same zip package but different PROCESSOR_NAME values.
Line continuations on Windows
The commands below use \ for line continuation (bash style). If you're using PowerShell, replace each \ with a backtick ` character. Alternatively, run these in Git Bash which ships with Git for Windows.
- Deploy the first Lambda function (deposit processor) by running:
aws lambda create-function --function-name deposit-processor \
--runtime python3.12 \
--role [[ROLE_ARN="arn:aws:iam::123456789012:role/lambda-rds-role"]] \
--handler lambda_function.lambda_handler \
--zip-file fileb://lambda.zip \
--timeout 30 \
--vpc-config SubnetIds=[[SUBNET_IDS="subnet-xxx,subnet-yyy"]],SecurityGroupIds=[[LAMBDA_SG_ID="sg-xxxxxxxxxxxxxxxxx"]] \
--environment "Variables={DB_HOST=[[RDS_ENDPOINT="banking-db.xxxxxxxxxx.us-east-1.rds.amazonaws.com"]],DB_USER=admin,DB_PASSWORD=your-db-password,DB_NAME=bankingdb,PROCESSOR_NAME=deposit-processor}"
- Deploy the second Lambda function (withdrawal processor) by running:
aws lambda create-function --function-name withdrawal-processor \
--runtime python3.12 \
--role [[ROLE_ARN="arn:aws:iam::123456789012:role/lambda-rds-role"]] \
--handler lambda_function.lambda_handler \
--zip-file fileb://lambda.zip \
--timeout 30 \
--vpc-config SubnetIds=[[SUBNET_IDS="subnet-xxx,subnet-yyy"]],SecurityGroupIds=[[LAMBDA_SG_ID="sg-xxxxxxxxxxxxxxxxx"]] \
--environment "Variables={DB_HOST=[[RDS_ENDPOINT="banking-db.xxxxxxxxxx.us-east-1.rds.amazonaws.com"]],DB_USER=admin,DB_PASSWORD=your-db-password,DB_NAME=bankingdb,PROCESSOR_NAME=withdrawal-processor}"
- Deploy the third Lambda function (transfer processor) by running:
aws lambda create-function --function-name transfer-processor \
--runtime python3.12 \
--role [[ROLE_ARN="arn:aws:iam::123456789012:role/lambda-rds-role"]] \
--handler lambda_function.lambda_handler \
--zip-file fileb://lambda.zip \
--timeout 30 \
--vpc-config SubnetIds=[[SUBNET_IDS="subnet-xxx,subnet-yyy"]],SecurityGroupIds=[[LAMBDA_SG_ID="sg-xxxxxxxxxxxxxxxxx"]] \
--environment "Variables={DB_HOST=[[RDS_ENDPOINT="banking-db.xxxxxxxxxx.us-east-1.rds.amazonaws.com"]],DB_USER=admin,DB_PASSWORD=your-db-password,DB_NAME=bankingdb,PROCESSOR_NAME=transfer-processor}"
What do these commands do?
- Each command creates a separate Lambda function from the same lambda.zip package.
- The --vpc-config places the function inside your default VPC so it can reach the RDS instance on the private network.
- The --timeout 30 gives the function 30 seconds to complete. VPC-attached functions need extra time for cold starts and database connections.
- The --environment sets the database credentials and the unique PROCESSOR_NAME that each function stamps on its records.
Seeing a 'role not found' error?
- Double-check that the --role value is the full ARN (starts with arn:aws:iam::), not just the role name.
- If you just created the role, wait 10-15 seconds for IAM propagation before retrying.
- Verify the role exists by running aws iam get-role --role-name lambda-rds-role.
Help me fix my Lambda create-function error.
Verify the Lambda VPC configuration
Each function needs to be attached to your VPC with the correct security group. Without this, the functions can't reach your RDS instance.
- Confirm the deposit processor's VPC configuration by running:
aws lambda get-function --function-name deposit-processor --query "Configuration.VpcConfig"
- Repeat for the other two functions:
aws lambda get-function --function-name withdrawal-processor --query "Configuration.VpcConfig"
aws lambda get-function --function-name transfer-processor --query "Configuration.VpcConfig"
What should I see?
Each command returns a JSON object showing SubnetIds and SecurityGroupIds matching the values you provided during creation. You should see your Lambda security group ID listed under SecurityGroupIds.
- Verify all three functions appear in your account by running:
aws lambda list-functions --query "Functions[?starts_with(FunctionName, 'deposit') || starts_with(FunctionName, 'withdrawal') || starts_with(FunctionName, 'transfer')].FunctionName"
You should see all three function names: deposit-processor, withdrawal-processor, and transfer-processor.
Missing a function from the list?
- Check that you ran all three create-function commands without errors. Scroll up in your terminal to look for error messages.
- Confirm you're in the correct AWS region. Run aws configure get region to check.
Help me find my missing Lambda function.
Your processing functions are deployed and connected to the VPC. Next, you'll wire them up to EventBridge so incoming transaction events get routed to the correct processor automatically.
Create EventBridge Custom Bus and Rules
Your three Lambda functions are deployed, connected to the VPC, and ready to process transactions. Now you need the routing layer that inspects each incoming event and sends it to the correct processor.
EventBridge uses event patterns to perform content-based routing. You define a rule with a JSON pattern, and EventBridge compares every event against that pattern. When a match is found, the event is delivered to the rule's target. In this step, you'll create a custom event bus and three rules that each match on a different transaction_type value.
In this step, get ready to:
- Create a custom EventBridge event bus for banking transactions.
- Define three content-based routing rules with event patterns.
- Connect each rule to its corresponding Lambda function.
Create the custom event bus and routing rules
Every AWS account has a default event bus that receives events from AWS services. For your own application events, you create a custom event bus. This keeps your banking transaction events separate from AWS service events.
- Create the custom event bus by running this command:
aws events create-event-bus --name banking-event-bus
What does this command do?
The create-event-bus command creates a new event bus named banking-event-bus. All your transaction events will be published to this bus, and your routing rules will evaluate events on this bus only.
- You should see a response containing an EventBusArn field. Note this ARN for reference.
- Now create the deposit routing rule by running:
aws events put-rule --name deposit-rule --event-bus-name banking-event-bus --event-pattern '{"source":["banking.transactions"],"detail-type":["TransactionCreated"],"detail":{"transaction_type":["deposit"]}}' --state ENABLED
What is an event pattern?
The --event-pattern flag defines a JSON filter. EventBridge compares each incoming event against this pattern. Only events where source equals banking.transactions, detail-type equals TransactionCreated, and detail.transaction_type equals deposit will match this rule.
The --state ENABLED flag makes the rule active immediately.
- The output includes a RuleArn field. Copy and save this ARN. You will need it when granting EventBridge permission to invoke the Lambda function.
- Create the withdrawal routing rule by running:
aws events put-rule --name withdrawal-rule --event-bus-name banking-event-bus --event-pattern '{"source":["banking.transactions"],"detail-type":["TransactionCreated"],"detail":{"transaction_type":["withdrawal"]}}' --state ENABLED
- Copy and save the RuleArn from the output.
- Create the transfer routing rule by running:
aws events put-rule --name transfer-rule --event-bus-name banking-event-bus --event-pattern '{"source":["banking.transactions"],"detail-type":["TransactionCreated"],"detail":{"transaction_type":["transfer"]}}' --state ENABLED
- Copy and save the RuleArn from the output.
- Verify all three rules are active by running:
aws events list-rules --event-bus-name banking-event-bus
You should see three rules listed, each with State set to ENABLED.
Not seeing all three rules?
Make sure each put-rule command uses --event-bus-name banking-event-bus. Without this flag, the rule is created on the default event bus instead of your custom bus.
Check for typos in the event pattern JSON. A missing bracket or quote will cause the command to fail.
Help me troubleshoot my EventBridge rules not appearing on banking-event-bus.
Attach Lambda targets and grant permissions
Rules define which events match, but targets define where matched events go. You need to attach each Lambda function as the target for its corresponding rule. You also need to grant EventBridge permission to invoke each function.
- First, get the ARN of the deposit-processor function by running:
aws lambda get-function --function-name deposit-processor --query 'Configuration.FunctionArn' --output text
- Copy the ARN from the output. It looks like arn:aws:lambda:<region>:<account-id>:function:deposit-processor.
- Attach the deposit-processor Lambda as the target for deposit-rule by running:
aws events put-targets --rule deposit-rule --event-bus-name banking-event-bus --targets '[{"Id":"deposit-target","Arn":"[[DEPOSIT_LAMBDA_ARN="arn:aws:lambda:REGION:ACCOUNT:function:deposit-processor"]]" }]'
What are the Id and Arn fields?
Each target needs a unique Id string (any label you choose) and the Arn of the resource that should receive the event. EventBridge supports up to 5 targets per rule.
- Get the withdrawal-processor ARN by running:
aws lambda get-function --function-name withdrawal-processor --query 'Configuration.FunctionArn' --output text
- Attach the withdrawal-processor Lambda as the target for withdrawal-rule by running:
aws events put-targets --rule withdrawal-rule --event-bus-name banking-event-bus --targets '[{"Id":"withdrawal-target","Arn":"[[WITHDRAWAL_LAMBDA_ARN="arn:aws:lambda:REGION:ACCOUNT:function:withdrawal-processor"]]" }]'
- Get the transfer-processor ARN by running:
aws lambda get-function --function-name transfer-processor --query 'Configuration.FunctionArn' --output text
- Attach the transfer-processor Lambda as the target for transfer-rule by running:
aws events put-targets --rule transfer-rule --event-bus-name banking-event-bus --targets '[{"Id":"transfer-target","Arn":"[[TRANSFER_LAMBDA_ARN="arn:aws:lambda:REGION:ACCOUNT:function:transfer-processor"]]" }]'
Targets are attached, but EventBridge still needs explicit permission to invoke each Lambda function. Without a resource-based policy, EventBridge invocations will fail silently.
- Grant EventBridge permission to invoke deposit-processor by running:
aws lambda add-permission --function-name deposit-processor --statement-id deposit-rule-invoke --action 'lambda:InvokeFunction' --principal events.amazonaws.com --source-arn [[DEPOSIT_RULE_ARN="arn:aws:events:REGION:ACCOUNT:rule/banking-event-bus/deposit-rule"]]
Why is a resource-based policy needed?
Lambda functions use resource-based policies to control which AWS services can invoke them. The --source-arn flag scopes the permission to a specific rule. This means only your deposit-rule can trigger this function, not any EventBridge rule in the account.
- Grant EventBridge permission to invoke withdrawal-processor by running:
aws lambda add-permission --function-name withdrawal-processor --statement-id withdrawal-rule-invoke --action 'lambda:InvokeFunction' --principal events.amazonaws.com --source-arn [[WITHDRAWAL_RULE_ARN="arn:aws:events:REGION:ACCOUNT:rule/banking-event-bus/withdrawal-rule"]]
- Grant EventBridge permission to invoke transfer-processor by running:
aws lambda add-permission --function-name transfer-processor --statement-id transfer-rule-invoke --action 'lambda:InvokeFunction' --principal events.amazonaws.com --source-arn [[TRANSFER_RULE_ARN="arn:aws:events:REGION:ACCOUNT:rule/banking-event-bus/transfer-rule"]]
- Verify that targets are correctly attached by running:
aws events list-targets-by-rule --rule deposit-rule --event-bus-name banking-event-bus
You should see a Targets array containing one entry with the Arn of your deposit-processor Lambda function.
- Repeat for the other two rules to confirm:
aws events list-targets-by-rule --rule withdrawal-rule --event-bus-name banking-event-bus
aws events list-targets-by-rule --rule transfer-rule --event-bus-name banking-event-bus
Each should show its corresponding Lambda function ARN in the targets array.
Seeing FailedEntryCount in put-targets output?
If the put-targets response shows a FailedEntryCount greater than 0, check that the Lambda ARN is correct and that the function exists in the same region as your event bus.
Ensure you included both the Id and Arn fields in the targets JSON. Both are required.
Help me fix my EventBridge put-targets command that is failing.
Your event-driven routing layer is complete. Three rules are listening on your custom bus, each matching a specific transaction type and forwarding to the right processor. Next up, you'll initialize the database and send test events through the pipeline.
Initialize Database and Test the Pipeline
You have a fully wired event-driven pipeline. Rules on your custom bus are ready to route events, and Lambda functions are waiting in the VPC to process them.
The final piece is the data layer. Your Lambda functions will try to insert rows into a transactions table that does not exist yet. In this step, you will create the database schema, fire test events through the pipeline, and verify that content-based routing worked by querying the results.
In this step, get ready to:
- Connect to RDS MySQL and create the database schema.
- Publish test banking events to your custom EventBridge bus.
- Verify that each transaction was routed to the correct processor.
Connect to RDS and create the transactions table
Your Lambda functions expect a database called bankingdb with a transactions table. You need to create both before the pipeline can process events.
- First, temporarily allow your local machine to reach the RDS instance. Find your public IP by running:
- Note the IP address printed in your terminal.
curl https://checkip.amazonaws.com
- Add a temporary inbound rule to the RDS security group so your machine can connect on port 3306:
aws ec2 authorize-security-group-ingress \
--group-id [[RDS_SG_ID="your rds-sg security group ID"]] \
--protocol tcp \
--port 3306 \
--cidr [[MY_IP="your public IP address"]]/32
Why do you need this rule?
The RDS security group currently only allows traffic from the Lambda security group. To connect with the mysql CLI client from your local machine, you need to temporarily allow your IP address on port 3306.
- Connect to your RDS MySQL instance by running:
mysql -h [[RDS_ENDPOINT="your RDS endpoint address"]] -P 3306 -u admin -p
- When prompted, enter the master password you set when creating the RDS instance.
- You should see the mysql> prompt, confirming you are connected.
Can't connect to the database?
- Double-check that the CIDR in your security group rule matches your actual public IP with a /32 suffix.
- Confirm your RDS instance has --publicly-accessible set to true. If not, you will need to modify it with aws rds modify-db-instance.
- Make sure the RDS instance status is available (not still creating or modifying).
Help me troubleshoot my RDS MySQL connection issue
- Create the database and table by running these SQL statements at the mysql> prompt:
CREATE DATABASE IF NOT EXISTS bankingdb;
USE bankingdb;
CREATE TABLE IF NOT EXISTS transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
transaction_id VARCHAR(50) NOT NULL,
transaction_type VARCHAR(20) NOT NULL,
account_number VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
processor VARCHAR(50) NOT NULL,
processed_at TIMESTAMP NOT NULL
);
What does this SQL do?
- Creates a database called bankingdb that matches the DB_NAME environment variable your Lambda functions use.
- Creates the transactions table with columns for the transaction ID, type, account, amount, which processor handled it, and a timestamp.
- The processor column is the key proof of routing. Each Lambda stamps its own PROCESSOR_NAME here.
- Verify the table was created by running:
DESCRIBE transactions;
- You should see seven columns listed (id, transaction_id, transaction_type, account_number, amount, processor, processed_at).
- Exit the MySQL client by typing exit and pressing Enter.
Publish test events to the custom bus
Now you will simulate banking transactions by publishing events to EventBridge. Each event includes a transaction_type field that the routing rules will match on.
- Create a file called test-events.json with the three test events:
[
{
"Source": "banking.transactions",
"DetailType": "TransactionCreated",
"Detail": "{\"transaction_id\": \"txn-001\", \"transaction_type\": \"deposit\", \"account_number\": \"ACC-1234\", \"amount\": 500.00}",
"EventBusName": "banking-event-bus"
},
{
"Source": "banking.transactions",
"DetailType": "TransactionCreated",
"Detail": "{\"transaction_id\": \"txn-002\", \"transaction_type\": \"withdrawal\", \"account_number\": \"ACC-5678\", \"amount\": 200.00}",
"EventBusName": "banking-event-bus"
},
{
"Source": "banking.transactions",
"DetailType": "TransactionCreated",
"Detail": "{\"transaction_id\": \"txn-003\", \"transaction_type\": \"transfer\", \"account_number\": \"ACC-9012\", \"amount\": 1000.00}",
"EventBusName": "banking-event-bus"
}
]
What is in each event?
- Source is set to banking.transactions to match the event pattern on each rule.
- DetailType is TransactionCreated so EventBridge knows what kind of event this is.
- Detail is a JSON string containing the transaction payload. The transaction_type field is what the rules pattern-match on to decide which Lambda receives it.
- EventBusName targets your custom bus instead of the default bus.
- Publish all three events to your custom bus by running:
aws events put-events --entries file://test-events.json
- You should see a JSON response with "FailedEntryCount": 0 and three entries, each with an EventId.
Seeing FailedEntryCount greater than 0?
- Check that EventBusName in your JSON exactly matches banking-event-bus.
- Verify that Source, DetailType, and Detail are all present in each entry. EventBridge rejects entries missing any of these three required fields.
- Make sure Detail is a valid JSON string (escaped quotes inside the string value).
Help me fix my failed put-events call to EventBridge
Verify the pipeline results
Your events are now flowing through the pipeline. EventBridge matches each event's transaction_type to a rule and invokes the corresponding Lambda. Each Lambda inserts a row into the database.
- Wait 10-15 seconds for the Lambda functions to process the events. VPC-attached Lambda functions can have a cold-start delay on first invocation.
- Reconnect to your RDS instance:
mysql -h [[RDS_ENDPOINT="your RDS endpoint address"]] -P 3306 -u admin -p
- Query the transactions table to see the routed results:
USE bankingdb;
SELECT transaction_id, transaction_type, account_number, amount, processor, processed_at
FROM transactions;
- You should see three rows. Each row shows a different processor value:
- txn-001 processed by deposit-processor.
- txn-002 processed by withdrawal-processor.
- txn-003 processed by transfer-processor.
What does this prove?
Each transaction type was routed to a different Lambda function based solely on the event pattern rules you configured. One event bus, three rules, three processors. This is content-based routing in action.
- For additional confirmation, check CloudWatch Logs. Exit the MySQL client and run:
aws logs filter-log-events \
--log-group-name /aws/lambda/deposit-processor \
--filter-pattern "processed by" \
--query "events[].message" \
--output text
- You should see a log message containing processed by deposit-processor. Repeat for withdrawal-processor and transfer-processor if you want to confirm all three.
No rows in the transactions table?
- Wait a bit longer. VPC-attached Lambda cold starts can take 15-30 seconds on first invocation.
- Check CloudWatch Logs for errors. If you see connection timeouts, verify the Lambda security group ID matches what you attached to the Lambda functions.
- Confirm the DB_HOST environment variable on your Lambda functions matches the RDS endpoint (check with aws lambda get-function-configuration --function-name deposit-processor).
Help me debug why my Lambda functions aren't writing to the RDS database
Your event-driven pipeline is fully operational. A single JSON event published to the bus triggers automatic routing to the correct processor, all without any direct service-to-service coupling. Next up, the Secret Mission will show you how to archive and replay these events.
Secret mission
Add Event Archive and Replay
Your pipeline processes transactions in real time. But what happens when something goes wrong and you need to reprocess historical events? In this secret mission, you will configure an EventBridge archive to capture all events and replay a past transaction back through the pipeline.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The RDS instance costs approximately $0.017/hour, so tearing down within 1-2 hours keeps costs under $0.10.
Cost warning
Your RDS db.t3.micro instance is accruing charges at approximately $0.017/hour (about $0.41/day). Lambda and EventBridge are essentially free at this scale. If you are done with the project, delete the RDS instance promptly to avoid unnecessary costs.
Resources you used:
- RDS MySQL instance (banking-db) with database bankingdb.
- 3 Lambda functions (deposit-processor, withdrawal-processor, transfer-processor).
- Custom EventBridge event bus (banking-event-bus) with 3 rules.
- 2 security groups (lambda-sg, rds-sg).
- 1 IAM role (lambda-rds-role).
- EventBridge archive (banking-archive) if Secret Mission completed.
- Local project directory with lambda_function.py and create_table.sql.
Keep everything running
No action needed. Choose this if you are still actively building or testing the pipeline. The RDS instance will continue accruing charges at approximately $0.41/day. Lambda and EventBridge are essentially free at this scale.
Pause - I'll come back to this later
Stop the RDS instance to eliminate hourly compute charges while preserving your data. Lambda functions and EventBridge resources have no ongoing cost when idle.
- Stop the RDS instance by running:
- This preserves your database and data but stops the compute charges. Storage charges still apply (minimal for 20 GB).
aws rds stop-db-instance --db-instance-identifier banking-db
When you are ready to resume, start the instance again with:
aws rds start-db-instance --db-instance-identifier banking-db
Delete - I don't want to use this again
Remove all project resources and start fresh. Run each command below in order.
Delete the EventBridge archive (if Secret Mission completed):
aws events delete-archive --archive-name banking-archive
Remove targets from each rule, then delete the rules:
aws events remove-targets --rule deposit-rule --event-bus-name banking-event-bus --ids "target-1"
aws events remove-targets --rule withdrawal-rule --event-bus-name banking-event-bus --ids "target-1"
aws events remove-targets --rule transfer-rule --event-bus-name banking-event-bus --ids "target-1"
aws events delete-rule --name deposit-rule --event-bus-name banking-event-bus
aws events delete-rule --name withdrawal-rule --event-bus-name banking-event-bus
aws events delete-rule --name transfer-rule --event-bus-name banking-event-bus
Delete the custom event bus:
aws events delete-event-bus --name banking-event-bus
Delete the Lambda functions:
aws lambda delete-function --function-name deposit-processor
aws lambda delete-function --function-name withdrawal-processor
aws lambda delete-function --function-name transfer-processor
Delete the RDS instance (skip final snapshot since this is a learning project):
aws rds delete-db-instance --db-instance-identifier banking-db --skip-final-snapshot
Wait for the RDS instance to finish deleting before removing the security groups:
aws rds wait db-instance-deleted --db-instance-identifier banking-db
Delete the security groups (replace the group IDs with your actual values):
aws ec2 delete-security-group --group-id [[LAMBDA_SG_ID="your lambda-sg group ID"]]
aws ec2 delete-security-group --group-id [[RDS_SG_ID="your rds-sg group ID"]]
Detach the managed policies from the IAM role, then delete the role:
aws iam detach-role-policy --role-name lambda-rds-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
aws iam detach-role-policy --role-name lambda-rds-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name lambda-rds-role
Delete local project files:
cd ~/Desktop
rm -rf lambda_function.py create_table.sql pymysql/ lambda.zip
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully operational event-driven banking transaction router using Amazon EventBridge, Lambda, and RDS.
You've learned how to:
- Built a custom EventBridge event bus with content-based routing rules that pattern-match on nested JSON fields to intelligently route banking transactions.
- Deployed Lambda functions in a VPC with security group connectivity to an RDS MySQL instance, proving you can wire up private networking between serverless compute and managed databases.
- Published custom events via the CLI and verified the full pipeline by querying the transactions table to confirm each event was processed by the correct handler.
- (Secret Mission) Configured an EventBridge archive and replayed historical transactions back through the pipeline, proving your system can recover and reprocess past events.
Ready to quiz yourself?