This repo walks you through building a complete data engineering project on AWS, from a live API all the way to a Redshift warehouse. You send retail transactions to an API, they stream through Kinesis, and you fan them out into raw S3 storage, a DynamoDB serving layer, and Redshift, plus a separate batch pipeline with Glue. By the end you'll have every piece running and you'll understand how they connect.
It follows the LearnDataEngineering course modules 05 to 11. You can do it start to finish, or jump to the module you care about.
Two data paths that both land in the same warehouse:
- The streaming path. A POST API takes single transactions, a Lambda drops them into a Kinesis stream called
APIData, and three consumers read off that same stream: one Lambda writes the raw JSON to S3, one Lambda writes into DynamoDB for fast lookups, and Amazon Data Firehose loads everything into Redshift. - The serving API. A GET API reads a single invoice back out of DynamoDB, so you have a read path for a dashboard or app.
- The batch path. CSV files sit in S3, a Glue crawler catalogs them, and a Glue ETL job bulk-loads them into a second Redshift table.
If you've heard the term "lambda architecture" (a batch layer and a speed layer feeding the same place), that's basically what this is. You don't have to care about the name. The point is you'll have built both styles of pipeline against the same data.
insert_template.py
│ POST
▼
API Gateway ──► Lambda (KinesisIngest) ──► Kinesis "APIData"
│
┌─────────────────────────────────┼─────────────────────────────────┐
▼ ▼ ▼
Lambda (KinesisToRaw) Lambda (KinesisToDynamoDB) Amazon Data Firehose
│ │ │
▼ ▼ ▼
S3 (raw JSON) DynamoDB Redshift
(Customers, Invoices) (firehosetransactions)
│
▼
Lambda (DynamoDBReadAPI) ◄── GET API Gateway
Batch path (separate):
S3 (CSV files) ──► Glue Crawler ──► Glue Data Catalog ──► Glue ETL Job ──► Redshift (bulkimport)
We use the classic Online Retail dataset: real transactions from a UK-based online store. Each row is one line item on an invoice, with these columns:
InvoiceNo, StockCode, Description, Quantity, InvoiceDate, UnitPrice, CustomerID, Country
The raw file Online_Retail.csv has around 540,000 rows and some messiness in it: missing customer IDs, cancelled invoices, and old Mac-style line endings that trip up some tools. So we clean it first with data_preprocessing.py, which drops the empty rows, keeps only numeric invoice numbers, and writes out clean versions you can actually stream.
You don't have to run the cleaning yourself, the cleaned files are already in the repo:
| File | Rows | Use it for |
|---|---|---|
Online_Retail_Cleaned.csv |
~397k | The full cleaned set |
Online_Retail_Cleaned_1000rows.csv |
1000 | Batch loading and bigger tests |
Online_Retail_Cleaned_10rows.csv |
10 | Your first end-to-end test |
Start with the 10-row file. When something breaks you want to see it break in ten records, not a thousand.
- An AWS account. A fresh free-tier account is fine, but a few things here cost money (see the cost note below).
- A region you'll stick to the whole way through. This guide uses
us-east-1(N. Virginia) everywhere. Region matters more than you'd think, because the Firehose IP and some service endpoints are region-specific. If you pick a different region, use it consistently. - Python 3.10+ on your machine for the local sender script, with
pandasandrequests:pip install pandas requests
- Basic comfort clicking around the AWS console. I do almost everything here in the UI on purpose, because seeing where each setting lives teaches you more than pasting Terraform you don't understand yet.
This is super important and it's the thing that bites people. A few of these services are not free-tier-friendly if you leave them running:
- Redshift (module 10) costs money per hour the cluster is up.
- Kinesis provisioned shards cost money per hour.
- DynamoDB provisioned capacity and Firehose add small amounts.
None of it is expensive for a day of learning, but a Redshift cluster you forgot about over a weekend will surprise you. So do two things: set up a billing budget in module 05 before anything else, and go through the Cleaning up section at the end when you're done. Pause the Redshift cluster when you're not actively using it.
All the code and config lives under Code, split by module:
Code/
├── Module-06/ ingestion: Lambda, sender script, IAM policy JSONs, the datasets
├── Module-07/ stream to S3: Lambda
├── Module-08/ stream to DynamoDB: Lambda + test event
├── Module-09/ read API: Lambda
├── Module-10/ Redshift + Firehose: table SQL, jsonpaths, copy command
└── Module-11/ batch with Glue: bulkimport table SQL
Before we build anything, two foundations: a budget so AWS can't bill you by surprise, and a clear picture of how IAM roles and policies work, because every Lambda in this project needs the right ones.
- In the console search bar, search for Billing and Cost Management. This is where you see your monthly costs.
- Go to Budgets and Planning → Budgets → Create Budget.
- Use a template, pick Monthly cost budget, and enter a name, an amount, and the email that should get the alert.
Watch out: a budget alert only fires at 85% of forecast, at 100% of forecast, and at 100% of actual spend. After that it goes quiet. So if you want to be warned earlier or more often, create a few budgets at different thresholds as a notification ladder, or set up a Budget Report. One budget is not a safety net, it's a single heads-up.
You'll create a bunch of these as we go, so get the model straight now:
- A policy is a set of permissions. "Allowed to put records into Kinesis." "Allowed to read from DynamoDB." That's it.
- A role is what you attach to a service, like a Lambda function. The role carries one or more policies.
So the pattern every single time is: write a policy that says what's allowed, then create a role for the Lambda and attach that policy plus a logging policy. If you go into the Policies section in IAM you'll see the AWS-managed ones marked with a little cube, and you can also write your own as JSON and scope them down to specific resources.
Here's which permissions each Lambda in this project ends up needing:
| Lambda | Needs to |
|---|---|
| Ingestion (module 06) | Put records into Kinesis, write logs to CloudWatch |
| Stream to raw S3 (module 07) | Read records from Kinesis, write to S3, log to CloudWatch |
| Stream to DynamoDB (module 08) | Read records from Kinesis, write to DynamoDB, log to CloudWatch |
| Read API (module 09) | Read from DynamoDB, log to CloudWatch |
We don't create all of these up front. We build each role right before the module that uses it, so it's less abstract and you can see exactly why that Lambda has those permissions. The exact JSON for the Kinesis and DynamoDB policies is in Code/Module-06 if you'd rather read the permissions than click through the UI.
Logging and the Boto3 basics from this module can stay as they are in the course.
This is the front of the whole thing. We build: a Kinesis stream, a Lambda that writes to it, an API Gateway endpoint that triggers the Lambda, and a local script that fires transactions at the API.
- Search for Kinesis and click Data Streams → Create Data Stream.
- Name it
APIData. This exact name matters, the Lambda code references it. - Capacity mode Provisioned, Shards 1. Provisioned is cheaper than on-demand for a small steady test like this.
- Scroll down and create the stream.
You should see the stream go to Active after a moment. One shard is plenty here. In production you'd size shards to your throughput, but a single shard handles our test traffic fine.
- Search for IAM, go to the Policies tab, Create Policy.
- Choose service Kinesis → Next, then under Write access level pick PutRecord and PutRecords.
- For Resources → Specific, paste the ARN of your
APIDatastream. Or pick "any in this account" if you want to keep it simple while learning. Then Next. - Name the policy
myKinesisWriteand create it. (This matchesIAM-Policy-WriteKinesis.json.) - Go to the Roles tab, Create Role, choose AWS service → Lambda → Next.
- Attach two policies:
myKinesisWriteandAWSLambdaBasicExecutionRole(that second one is the CloudWatch logging permission). - Name the role
LambdaStreamIngestand create it.
- Search for Lambda → Create a function.
- Function name
KinesisIngest, runtime Python 3.12, architecture default x86. - Under permissions, Change default execution role → Use an existing role →
LambdaStreamIngest. - Create the function.
- Scroll to the code, paste in
PostMethod-Lambda-Code.py, and hit Deploy.
The code is short: it reads the POST body, turns it back into a string, and calls put_record into the APIData stream. Nothing magic.
- Search for API Gateway → Create API → REST API → Build.
- API name
Online Retail Data Transfer, create it. - Create Resource, name it
main, create resource. - Create method: method type POST, integration type Lambda function, then search for
KinesisIngestand select it. Create the method.
Now the piece people forget, and it breaks everything downstream if you skip it:
- Click Integration Request → Edit, scroll to Mapping Templates, and Generate template → Method request passthrough. Set content type to
application/jsonand Save.
This is super important. The Lambda reads
event['context']['http-method']andevent['body-json']. Those fields only exist because of this passthrough mapping template. Skip it and you'll get a 200 in the test console but aKeyErrorin the logs, and you'll waste an afternoon on it. Ask me how I know.
- Deploy API, create a stage called
prod, and deploy. In a real project you'd have several stages through development, one is fine for us.
- Go to the Test section of your POST method.
- Paste this into the request body:
{"InvoiceNo":536365,"StockCode":"84029E","Description":"RED WOOLLY HOTTIE WHITE HEART.","Quantity":6,"InvoiceDate":"12/1/2010 8:26","UnitPrice":3.39,"CustomerID":17850,"Country":"United Kingdom"} - Run it. You should get a 200 response, because the stream and the function already exist.
- Check the logs under CloudWatch → Log groups →
/aws/lambda/KinesisIngestto see the event come through.
Now let's fire actual rows at it with insert_template.py.
- Grab your invoke URL. Go to API Gateway → Stages and copy the URL, then add your resource name to the end. So it looks like
https://xxxxx.execute-api.us-east-1.amazonaws.com/prod/main. - Open
insert_template.pyand paste it into theURLvariable:# replace this with your own invoke URL, and don't forget the /main resource on the end URL = "https://xxxxx.execute-api.us-east-1.amazonaws.com/prod/main"
- Run it from inside
Code/Module-06so it finds the CSV next to it:cd Code/Module-06 python3 insert_template.py
The script reads Online_Retail_Cleaned_10rows.csv by default and posts each row. You should see each row's JSON print followed by <Response [200]>. If you want to push more, point the script at the 1000-row file instead, but do the 10-row run first.
To confirm the data actually landed in Kinesis, open the APIData stream and watch the PutRecord count climb in the Monitoring tab, or check the CloudWatch logs again.
Bonus: you can test the API with Postman instead of code if you just want to fire one sample. The ready-made JSON is in
Postman_test_string.txt.
First consumer off the stream. We store every incoming record as raw JSON in S3, which is your cheap, keep-everything landing zone.
- Search for S3 → Create bucket.
- Give it a name. Bucket names have to be globally unique across all of AWS, so something generic will be taken. See the naming rules. Example:
online-retail-apidata-raw. - Leave everything on default: block public access on, no ACLs. Create the bucket.
- In IAM → Policies → Create Policy, choose Kinesis → Next.
- Under access levels, select all List actions and all Read actions. This Lambda only reads the stream, it never writes to it.
- Resources: your
APIDataARN, or any in this account. Next. - Name it
myKinesisReadand create it. (MatchesIAM-Policy-ReadKinesis.json.) - Go to Roles → Create Role → AWS service → Lambda → Next.
- Attach three policies:
myKinesisRead,AmazonS3FullAccess, andAWSLambdaBasicExecutionRole. - Name the role
LambdaRawS3Pipelineand create it.
This time we start from an AWS blueprint that already wires up the Kinesis trigger for us.
- Lambda → Create a function → Use a blueprint.
- Blueprint name: Process records sent to a Kinesis stream (runtime Python 3.10), architecture default x86.
- Name it
KinesisToRaw(orKinesisToS3, your call). - Change default execution role → Use an existing role →
LambdaRawS3Pipeline. - Configure the Kinesis trigger:
- Select the
APIDatastream. - Batch Size 100. That means each Lambda invocation handles up to 100 records, so you get small files of ~100 entries. Fine for learning, you'd tune it up for real volume.
- Additional settings: Retry attempts: 2.
- Select the
- Create the function.
The Lambda reads the bucket name from an environment variable, so it isn't hardcoded.
- Go to the function's Configuration → Environment variables → Edit → Add.
- Key
bucket_name, value your actual bucket name (for exampleonline-retail-apidata-raw). Save. - Scroll to the code, paste in
Module7-Lambda-Code.py, and Deploy.
The code decodes each base64 Kinesis record, collects the batch, and writes it as a timestamped file like data_2025-08-12-14-30-00.json into your bucket.
- Click the dropdown next to Test → Configure new test event.
- Give it a name and pick the Kinesis template (
kinesis-get-records). Save. - Hit Test. You should see a success message in the output window, and a new JSON file should appear in your S3 bucket.
If you already have live data flowing from module 06, the trigger fires on its own and files start showing up without you doing anything. That's the whole point of the trigger.
Second consumer off the same stream. Here we write into DynamoDB, which gives us fast key lookups for a serving API later. Same stream, different destination, and that's the nice thing about Kinesis: multiple consumers, one source.
We need two tables. Create each one the same way under DynamoDB → Tables → Create Table:
Customers
- Table name:
Customers - Partition key:
CustomerID, type Number - Table settings: Customized
- Capacity mode Provisioned, autoscaling off, read capacity 1, write capacity 1
- Create table
Invoices
- Same steps, but table name
Invoicesand partition keyInvoiceNo, type Number
Capacity 1/1 keeps cost near nothing. It's slow if you hammer it, but you won't during a tutorial.
- IAM → Policies → Create Policy, choose DynamoDB → Next.
- Under List pick ListTables. Under Write pick UpdateItem and PutItem.
- Resources: any in this account. Name it
myDynamoDBWriteand create it.
Note:
PutItemreplaces a whole row,UpdateItemonly touches the columns you give it and leaves the rest alone. The Lambda here usesUpdateItemso it can keep adding stock codes to an existing invoice without wiping what's already there. Worth knowing the difference, it changes how your data builds up.
- Roles → Create Role → AWS service → Lambda → Next.
- Attach
myDynamoDBWrite,myKinesisRead(this Lambda reads the stream too), andAWSLambdaBasicExecutionRole. - Name the role
LambdaDynamoDBWriteand create it.
- Lambda → Create a function → Use a blueprint, same Process records sent to a Kinesis stream blueprint (Python 3.10), x86.
- Name it
KinesisToDynamoDB. - Use an existing role →
LambdaDynamoDBWrite. - Kinesis trigger: select
APIData, Batch Size 10 this time for faster debugging, retry attempts 2. - Create the function, then paste in
Module8-Lambda-Code.pyand Deploy.
The code splits each transaction across both tables: it updates the Customers row keyed on CustomerID, and it updates the Invoices row keyed on InvoiceNo, storing the line item as a JSON blob under its stock code.
- Configure a new test event, but this time use the ready-made event: paste the contents of
DynamoDBTestEvent.json. - Run the test. You should see it process the records successfully.
- Go to DynamoDB → Tables →
Customers(orInvoices) → Explore table items and you should see rows.
Now the serving side. We build a GET endpoint that pulls a single invoice back out of DynamoDB, so a dashboard or app has something to call.
- IAM → Policies → Create Policy → DynamoDB → Next.
- Under Read access level pick GetItem. That's all this one needs. Resources: any in this account. Name it
myDynamoReadand create it. (MatchesIAM-Policy-Get-DyanamoDB.json.) - Roles → Create Role → AWS service → Lambda → Next, attach
myDynamoReadandAWSLambdaBasicExecutionRole, name itLambdaDynamoDBRead, create it.
- Lambda → Create a function, name
DynamoDBReadAPI, runtime Python 3.12, x86. - Use an existing role →
LambdaDynamoDBRead, create the function. - Paste in
GetMethod-Lambda-Code.pyand Deploy.
It reads the InvoiceNo from the query string and does a get_item against the Invoices table.
We reuse the API from module 06, we're just adding a second method to the same main resource.
- API Gateway → your
Online Retail Data TransferAPI → resourcemain. - Create method: type GET, integration type Lambda function, search for
DynamoDBReadAPI, create the method. - Integration Request → Edit → Mapping Templates → Method request passthrough, content
application/json, save. Same reason as the POST method: the Lambda readsevent['params']['querystring'], which only exists with this template. - Deploy API to the
prodstage. - Test it: in the Test section, add the query string
InvoiceNo=536365and run. You should get a 200 back with the invoice item as JSON.
Third consumer off the stream. Amazon Data Firehose reads from Kinesis and loads into a Redshift warehouse, so you have your data in real SQL tables for analytics. This is the module with the most moving parts, so go slow.
- Search for Redshift → Clusters → Create Cluster.
- Cluster identifier: leave it
redshift-cluster-1. - Size:
dc2.large, number of nodes 1. - Admin user name
awsuser, and set an admin password you'll remember. You'll type it into Firehose and Glue later. - Database name defaults to
dev. Leave it, or change it under Additional configuration if you want. - Open Additional configuration → Network and security and check Publicly accessible. Firehose needs to reach it.
- Create the cluster.
No IAM role is needed on Redshift itself here, because we aren't having Redshift reach out to anything. Firehose pushes the data in.
Watch out: once the cluster is up, create a snapshot before you try to pause it (Actions → Create snapshot). AWS won't let you pause a cluster that has never been snapshotted, and pausing is how you stop the meter running when you take a break.
Redshift sits inside a VPC, and Firehose has to be allowed in.
- Redshift → your cluster → Properties tab, scroll to network and security, click the VPC security group.
- Select the security group, scroll to Inbound rules → Edit inbound rules, and add a rule.
- The rule allows the Firehose IP for your region. For
us-east-1(N. Virginia) it's52.70.63.192/27. For any other region, get the right IP from the Firehose access docs, because it's different per region and a wrong one here means data silently never arrives.
Heads up: the security group ID will look a little different from any screenshot. That's normal, it's generated per account. Just make sure you're editing the one attached to your cluster.
- Redshift → Query editor, connect to the database. Create a new connection, database name
dev, and your admin credentials. - Run the create-table SQL from
Redshift-Table-Create-Command.txt. It creates thefirehosetransactionstable. - Click the three dots next to the table to view its schema and confirm it's there.
The columns are all set to not null on purpose. That way a row that didn't map correctly fails loudly on import instead of quietly landing half-empty. If loads fail, that same file tells you to check STL_LOAD_ERRORS for the reason.
Firehose stages the data in S3 before it copies it into Redshift, and it needs a map from your JSON fields to the table columns.
- Create another S3 bucket (globally unique name), for example
firehoseredshift-yourname. Defaults are fine. - Download
jsonpaths.jsonfrom this repo. It lists each field in column order. - Upload that file into your new bucket with the Upload button.
- Search for Amazon Data Firehose → Firehose streams → Create Firehose stream.
- Source: Amazon Kinesis Data Streams, destination: Amazon Redshift.
- Source settings: browse for the
APIDatastream. - Leave the stream name on its default
KDS-RED-.... - Destination settings: Provisioned cluster →
redshift-cluster-1, databasedev. - Authentication: username and password, user
awsuserand your password. - Table:
firehosetransactions. - Intermediate S3 bucket: browse for the
firehoseredshiftbucket you just made. - Copy command options: use the JSON copy command, which references your
jsonpaths.json. The template is inFirehose-copy-command.txt.
Do NOT skip this: you have to edit the bucket name in the copy command to your actual bucket. The template ships with a placeholder bucket, and if you leave it, the copy points at a bucket that isn't yours and nothing loads. This is the single most common reason "everything looks configured but Redshift stays empty."
- Retry duration: 120.
- Open Buffer hints, compression and encryption and set buffer size 2 MB, buffer interval 60 seconds, so data flushes fast enough for you to see it during testing.
- In Advanced settings, let it create a Service Role with the permissions Firehose needs.
- Create the Firehose stream.
Now send some data through module 06's script again and give it a minute. Rows should show up in firehosetransactions when you query it in the Redshift editor. If they don't, check STL_LOAD_ERRORS first, it almost always names the exact problem.
Completely separate path from everything above. Instead of streaming, we take CSV files sitting in S3 and bulk-load them into Redshift with Glue. This is your batch layer.
Glue needs to reach S3 from inside the VPC, and a Gateway endpoint is the clean way to do that.
- Search for VPC → Endpoints → Create Endpoint.
- Name
s3endpoint, category AWS services. - Search services for S3 and pick the Gateway one for your region. For
us-east-1that'scom.amazonaws.us-east-1.s3, type Gateway. - VPC: the one your Redshift cluster is in (usually the only default one).
- Check the box for the default route table, set policy to Full access, and create it.
- IAM → Roles → Create Role → AWS service → Glue → Next.
- Attach
AmazonRedshiftFullAccess,AWSGlueServiceRole, andAmazonS3FullAccess. - Name it
AWSGlueServiceRole-bulkimportand create it.
- Create an S3 bucket, for example
aws-bulkimport-glue(unique name, defaults fine). - Upload
Online_Retail_Cleaned_1000rows.csvwith the Upload button. This is the batch we'll load.
Make sure Redshift is running first: Redshift → your cluster → Actions → Resume if you paused it.
Create a Glue database to catalog into:
- Glue → Data Catalog → Databases → Create Database, name
glue-transactionsdb, create it.
Now a crawler for the CSV files in S3:
- Crawlers → Create Crawler, name it something unique like
S3LearnDataEngineeringCrawler, Next. - Add a data source: S3, browse for your
aws-bulkimport-gluebucket, add it as a source, Next. - Choose the existing IAM role
AWSGlueServiceRole-bulkimport, Next. - Target database
glue-transactionsdb, schedule on demand, Next, create the crawler. - Run it to test. It takes a few minutes.
Then a connection and a second crawler for Redshift:
- Connections → Create connection → Amazon Redshift → Next. Pick your cluster, database
dev, and your credentials. Name it something likeRedshift connectionand create it. Then open it, Actions → Test connection, pick the IAM role, and test. - Create a second crawler (
RedshiftLearnDataEngineeringCrawler), data source JDBC, include path pointing at the Redshift table you want to catalog (for exampledev/public/bulkimport). Same IAM role, same target database, on demand, create and run it. - Check Tables in the left pane to see the cataloged tables show up.
Before this job can write to Redshift, the target table has to exist. Run the create-table SQL from Redshift-Table-Create-Command.txt in the Redshift query editor first. It creates the bulkimport table.
- Glue → ETL Jobs → Create a job with Visual ETL.
- Source: add an S3 source, point it at your bucket and the CSV format, and set the IAM role. Click the + to add the next node.
- Transform: add a Change Schema node and set the column data types so they match the target table.
- Target: add a Redshift target, pick your connection, database, and the
bulkimporttable, and set the write mode to APPEND (insert). - Go to the Job details tab, name it something like
Redshift Bulkimport, set the IAM role, and limit workers to 2 so it stays cheap. - Save, then Run the job.
When it finishes, query bulkimport in Redshift and you should see your 1000 rows. That's your batch pipeline done, sitting right next to the streaming one, both feeding the same warehouse.
The stuff that actually goes wrong when you build this. Most of it is small and maddening, which is exactly why it's worth writing down.
You get a 403 from the API when running insert_template.py
You almost certainly left the resource name off the URL. The URL you copy from the stage in the console does not include your main resource, so you have to add it yourself. It should end in /prod/main, not just /prod.
200 in the console test, but the CloudWatch log shows KeyError: 'context'
Two causes. Either you skipped the "Method request passthrough" mapping template on the Integration Request (that's what creates the context and body-json fields), or you sent an empty request with no payload. Add the mapping template, and make sure you're actually sending data.
json-body not found error
Same root cause: the application/json mapping template isn't configured on the method. Go back to Integration Request and add it.
Data flows through the API but nothing lands in S3 or DynamoDB
Check that the consumer Lambda's Kinesis trigger points at the APIData stream and is enabled, and that its role has myKinesisRead. Also check the Lambda's own CloudWatch logs, a permissions error shows up there clearly.
Firehose is configured but Redshift stays empty
Work through these in order. Did you edit the bucket name in the copy command to your real bucket? Is the Firehose IP for your region added to the Redshift security group inbound rules? Is the cluster publicly accessible? Then query STL_LOAD_ERRORS in Redshift, which usually names the exact column or row that failed.
S3 "bucket already exists" error Bucket names are globally unique across all of AWS, not just your account. Someone has that name. Add something specific like your username or a random suffix.
Can't pause the Redshift cluster You need to create a snapshot first (Actions → Create snapshot). AWS blocks pausing a cluster that's never been snapshotted.
The raw Online_Retail.csv looks like one giant line
It uses old Mac carriage-return line endings, so some tools show it as a single line. You don't need to touch the raw file, use the cleaned versions in the repo, or run data_preprocessing.py yourself.
Don't skip this, it's the difference between a free learning project and a surprise bill.
- Pause or delete the Redshift cluster. This is the big one. Snapshot it, then pause it, or delete it entirely if you're done.
- Delete the Kinesis stream
APIDataso you stop paying for the provisioned shard. - Disable or delete the Lambda triggers and functions if you're finished.
- Empty and delete the S3 buckets you created (raw, firehose staging, bulk import).
- Delete the DynamoDB tables
CustomersandInvoices. - Delete the Firehose stream.
Leave the budget from module 05 in place. It costs nothing and it'll warn you if you missed something.
If a step didn't work or something in here is out of date because AWS moved a button around again, open an issue on this repo and I'll take a look.
For the full course and more data engineering material:
- LearnDataEngineering Academy: learndataengineering.com
- YouTube: @andreaskayy
Have fun building this. Once both pipelines are running and you can see the same data arrive two different ways, you'll have a real feel for how streaming and batch fit together, and that's worth a lot more than any single service on your resume.
Andreas