Cloud Ops Ticketing API on AWS

Build a serverless REST API to manage cloud ops tickets with DynamoDB and Lambda.

Introduction

30 Second Summary

When a website goes down or a service stops working, someone needs to track the problem until it is fixed. Ticketing systems are how operations teams manage every incident from report to resolution.

In this project, you will build a serverless cloud operations ticketing API on AWS. You will create, retrieve, update, and list incident tickets through a live API you call from the command line.

What You'll Build

You will call your live API from PowerShell, submit a ticket for a production database outage, and watch the response come back with a unique ticket ID and timestamp.

By the end of this project, you'll have:

  • A live REST API backed by DynamoDB for creating, retrieving, updating, and listing operations tickets with priority levels and automatic timestamps.
  • A ticket lifecycle where you update a ticket's status from Open to In Progress to Resolved, the same workflow real ops teams follow during incidents.
  • A hands-on demo connecting Lambda and API Gateway that you can walk through in a cloud engineering interview.
  • Secret Mission: Wire up Amazon SNS email notifications so Critical tickets automatically trigger an alert when created.

Are there any prerequisites?

You need an AWS account to complete this project. No local installations are required. All work happens in the AWS Console and PowerShell, which comes pre-installed on Windows.

Before We Start

Before you open the AWS Console, let's lock in what you're about to build and why it matters.

Create Your Ticket Database

Every ticketing system needs a place to store tickets. Before you can write any code, you need a database that holds ticket data like titles, priorities, and statuses.

You'll use Amazon DynamoDB, a NoSQL database that requires zero server management. Its key-value model maps naturally to ticket records. Each ticket gets a unique ID as its key, and all the ticket details (title, priority, status) are the value.

In this step, get ready to:
  • Create a DynamoDB table to store your operations tickets.
  • Add a test ticket manually to confirm the table works.
  • Scan the table to read your data back.
Create a DynamoDB table

DynamoDB tables live inside the AWS Management Console. Your first task is to sign in and navigate to the DynamoDB service.

  • Open your browser and go to console.aws.amazon.com.
  • Sign in with your AWS account credentials.
  • In the search bar at the top of the console, type DynamoDB and select DynamoDB from the results.

What is DynamoDB?

DynamoDB is a fully managed NoSQL database service from AWS. Unlike traditional databases, you don't need to set up or maintain any servers.

It stores data as items (similar to rows) inside tables. Each item is a collection of attributes (similar to columns), but different items in the same table can have different attributes. This flexibility is perfect for tickets, where some tickets might have fields that others don't.

Now you'll create the table that stores your operations tickets.

  • Click Create table.
  • In the Table name field, enter ops-tickets.
  • In the Partition key field, enter ticketId.
  • Leave the key type set to String.
  • Under Table settings, keep Default settings selected.
  • Click Create table.

The DynamoDB console returns you to the tables list. Wait a few seconds, then refresh the page. Your ops-tickets table's Status should show Active.

What is a partition key?

A partition key is the primary way DynamoDB identifies each item in a table. Every item must have a unique partition key value. In your case, ticketId is the partition key, so every ticket gets a unique ID like TICKET-001.

Why keep default settings?

Default settings use provisioned capacity mode with 5 read capacity units (RCU) and 5 write capacity units (WCU). This qualifies for the DynamoDB Always Free tier, which gives you 25 RCU, 25 WCU, and 25 GB of storage per month at no cost.

Table stuck in Creating status?

Table creation usually takes a few seconds. Try refreshing the page. If the table stays in Creating status for more than a minute, check that you're in the correct AWS region by looking at the region dropdown in the top right corner of the console.

Still stuck? Help me troubleshoot my DynamoDB table creation

Add a test ticket to your table

Your table exists, but it's empty. To confirm it works end to end, you'll manually add a test ticket using the console's built-in item editor.

  • Click on the ops-tickets table name to open it.
  • Click Explore table items.
  • Click Create item.
  • You'll see a ticketId field already listed. Enter TICKET-001 as its value.
  • Click Add new attribute and select String.
  • Enter title as the attribute name and Test ticket as its value.
  • Click Add new attribute again, select String, and add status with the value Open.
  • Click Add new attribute one more time, select String, and add priority with the value High.
  • Click Create item.

The console returns you to the items view. You should see your new item listed under Items returned with all four attributes: ticketId, title, status, and priority.

Don't see your item?

Make sure you clicked Create item at the bottom of the form (not Add new attribute). If the item still doesn't appear, click Run in the scan/query section to refresh the items list.

Help me troubleshoot adding items to DynamoDB

Scan the table to confirm your data

You can see the item in the console, but can the table actually return data when queried? A scan operation reads every item in the table and returns them all. This is the same operation your Lambda function will use in the next step.

Before you run the scan, take a guess. How many items do you expect the scan to return?

  • In the Explore table items view, make sure Scan is selected (not Query).
  • Click Run.

The scan returns your test ticket with all four attributes. You should see one item: TICKET-001 with title = Test ticket, status = Open, and priority = High. Your database is working and ready to serve data to your Lambda function.

What is a scan?

A scan reads every item in your table. It's the simplest way to retrieve data, but it's not the most efficient for large tables because it reads everything regardless of what you're looking for.

For a small operations ticketing system, a scan works fine. In production systems with thousands of items, you'd use queries with indexes to retrieve specific items faster.

Your ticket database is live and holding data. Next up, you'll build a Lambda function that can create and list tickets in this table through code, so you don't have to add items manually every time.

Build the Ticket Handler Function

Your ops-tickets table is live and holding data. You even ran a scan to confirm you can read items back. But right now, the only way to add or read tickets is by clicking through the DynamoDB console.

In this step, you'll create an AWS Lambda function that talks to DynamoDB on your behalf. It will know how to create new tickets and list all existing ones. This is the first time you'll see your project do something through code instead of manual clicks.

In this step, get ready to:
  • Create a Lambda function with DynamoDB permissions.
  • Add Python handler code for creating and listing tickets.
  • Test both operations from the Lambda console.
Create the Lambda function

Lambda lets you run code without provisioning a server. You give it a Python file, and AWS handles everything else. You also need to give the function permission to read and write to your DynamoDB table.

  • In the AWS Console search bar at the top, type Lambda and select Lambda from the results.
  • Click Create function.
  • Select Author from scratch.
  • For Function name, enter ops-ticket-handler.
  • For Runtime, select Python 3.13.
  • Expand the Change default execution role section.
  • Select Create a new role from AWS policy templates.
  • For Role name, enter ops-ticket-handler-role.
  • In Policy templates, search for and select Simple microservice permissions.
  • Click Create function.

The Lambda console opens your new function's page with a default code editor.

What did you just set up?

You created a Lambda function with a dedicated IAM role. The Simple microservice permissions policy template gives the function permission to interact with DynamoDB tables and write logs to CloudWatch. This means your function can read and write ticket data without any extra configuration.

Now you'll replace the default code with handler logic that creates and lists tickets. The code is broken into three sections below so you can understand each part before deploying.

  • In the code editor on the Code tab, select all the default code in lambda_function.py and delete it.
  • Paste the following imports and function setup as the first section of your handler:
import json
import boto3
import uuid
from datetime import datetime, timezone

# Connect to the DynamoDB ops-tickets table
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("ops-tickets")

def lambda_handler(event, context):
    # Log the incoming event for debugging
    print(event)
    status_code = 200
    headers = {"Content-Type": "application/json"}
    body = {}

    try:
        # Determine which API route was called
        route_key = event.get("routeKey", "")

What does this code do?

  • boto3 is the AWS SDK for Python. It comes pre-installed in the Lambda runtime, so you don't need to add any dependencies.
  • dynamodb.Table("ops-tickets") connects to the DynamoDB table you created in Step 1.
  • route_key will tell the handler which API operation the caller wants. For example, POST /tickets means "create a ticket."
  • Directly below the route_key line, paste the POST handler that creates new tickets:
        if route_key == "POST /tickets":
            request_body = json.loads(event.get("body", "{}"))
            # Generate a unique ticket ID
            ticket_id = "TICKET-" + str(uuid.uuid4())[:8].upper()
            now = datetime.now(timezone.utc).isoformat()
            # Validate the priority level
            priority = request_body.get("priority", "Medium")
            allowed_priorities = ["Low", "Medium", "High", "Critical"]
            if priority not in allowed_priorities:
                status_code = 400
                body = {"error": f"Invalid priority. Allowed values: {allowed_priorities}"}
            else:
                # Build the ticket item and save it to DynamoDB
                item = {
                    "ticketId": ticket_id,
                    "title": request_body.get("title", "Untitled"),
                    "description": request_body.get("description", ""),
                    "priority": priority,
                    "status": "Open",
                    "createdAt": now,
                    "updatedAt": now,
                }
                table.put_item(Item=item)
                body = {"message": "Ticket created", "ticket": item}

What does the POST handler do?

  • Each ticket gets a unique ID like TICKET-A3F2B1C0 using Python's uuid module.
  • The handler validates the priority field against four allowed values: Low, Medium, High, and Critical. Invalid priorities return a 400 error.
  • table.put_item(Item=item) writes the ticket directly to your ops-tickets DynamoDB table.
  • Below the POST handler, paste the GET handler and the rest of the function to complete it:
        elif route_key == "GET /tickets":
            # Scan returns all items in the table
            scan_result = table.scan()
            body = scan_result.get("Items", [])

        else:
            status_code = 400
            body = {"error": f"Unsupported route: {route_key}"}

    except Exception as e:
        status_code = 500
        body = {"error": str(e)}

    # Return the response with status code and JSON body
    return {
        "statusCode": status_code,
        "headers": headers,
        "body": json.dumps(body, default=str),
    }

What does the rest of the function do?

  • The GET handler calls table.scan() to retrieve every ticket in the table and returns them as a list.
  • The else block catches any route the function doesn't recognize and returns a 400 error.
  • The except block catches unexpected failures and returns a 500 error so the function never crashes silently.
  • json.dumps(body, default=str) converts the response to JSON. The default=str argument handles DynamoDB's special number types that standard JSON can't serialize.
  • In the DEPLOY section above the code editor, click Deploy to update your function's code.

You should see a confirmation banner at the top of the editor indicating the function has been updated.

Don't see the Deploy button?

  • Make sure you're on the Code tab of your function. The DEPLOY section sits above the code editor.
  • Check that you pasted all three sections in order. Missing a closing bracket or the return statement will cause a syntax error.
  • help me deploy my Lambda function code

✔️ Awesome, I've got everything!

Great. Double check you've clicked Deploy and seen the confirmation banner before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your code contains all three sections in this order:

  • Section 1: Imports (json, boto3, uuid, datetime), DynamoDB table connection, and the lambda_handler function signature through route_key.
  • Section 2: The POST /tickets handler with priority validation and put_item.
  • Section 3: The GET /tickets handler, the else fallback, the except block, and the return statement.

Make sure all the indentation is consistent. Python is whitespace-sensitive, so a misplaced space can break the function.

Test creating a ticket

Your function is deployed, but you haven't proven it actually works. The Lambda console lets you send test events that simulate real API requests.

  • Click the Test tab on your function's page.
  • Under Test event, select Create new event.
  • For Event name, enter createTicketTest.
  • Replace the default JSON in the Event JSON section with the following test event:
{
  "routeKey": "POST /tickets",
  "body": "{\"title\":\"Server CPU High\",\"description\":\"Web server CPU at 95%\",\"priority\":\"Critical\"}"
}

What is this test event?

This JSON simulates what API Gateway will send to your function later. The routeKey tells the handler which route was called. The body contains the ticket details as a JSON string, just like a real HTTP request body.

  • Click Save changes.

Before you run this test, what do you expect to see? Will the function create a ticket and return a 200, or will something go wrong?

  • Click Test.

Under Execution result, expand Details. You should see a response with statusCode of 200 and a body containing your new ticket data, including a generated ticketId and status of Open.

  • Confirm the ticket was saved to DynamoDB by navigating back to the DynamoDB console.
  • Click Tables, then select ops-tickets.
  • Click Explore table items. You should now see two items: your original TICKET-001 test ticket and the new ticket your function just created.

Seeing an error in the execution result?

  • If you see an AccessDeniedException, the IAM role may not have DynamoDB permissions. Go to the Configuration tab, then Permissions, and confirm the role ops-ticket-handler-role is attached with the Simple microservice permissions policy.
  • If you see a syntax error, go back to the Code tab and check that all three code sections are pasted in order with consistent indentation.
  • help me fix my Lambda function test error
Test listing all tickets

Creating tickets works. Now you need to confirm the function can also retrieve them.

  • Navigate back to the Lambda console and open your ops-ticket-handler function.
  • Click the Test tab.
  • Under Test event, select Create new event.
  • For Event name, enter listTicketsTest.
  • Replace the default JSON with this test event:
{
  "routeKey": "GET /tickets"
}
  • Click Save changes.

How many tickets do you think the GET handler will return? Remember what's in your table right now.

  • Click Test.

Expand Details under Execution result. The response body should contain a list with at least two tickets: your original TICKET-001 and the one your POST test just created.

Only seeing one ticket?

  • Make sure you selected the listTicketsTest event, not your earlier createTicketTest event. Running the wrong event would create another ticket instead of listing them.
  • If the POST test failed earlier, there may only be one ticket (your manually created one). Go back and fix the POST test first.
  • help me list tickets from my Lambda function

Your Lambda function can now create and list tickets through code. In the next step, you'll give it a public URL so you can call it from anywhere.

Expose Your API to the World

Your Lambda function can create and list tickets, but only when you manually trigger it from the Lambda console's test tool. Nobody outside AWS can reach it.

In this step, you will use API Gateway to give your Lambda function a public URL. Once it has one, you can call your API from any computer using curl.exe, the same way real APIs work.

In this step, get ready to:
  • Create an HTTP API in the API Gateway console with routes and a Lambda integration.
  • Create a ticket by calling your live API from PowerShell.
  • List all tickets from the live API to confirm everything works end to end.
Create the HTTP API in API Gateway

API Gateway is the front door for your Lambda function. It receives incoming HTTP requests, routes them to Lambda, and returns the response.

  • In the AWS Console search bar at the top, type API Gateway and select it from the results.
  • Click Create API.
  • Find the HTTP API card and click Build.

Why HTTP API instead of REST API?

AWS offers two types of API Gateway APIs. HTTP APIs are simpler to set up, about 70% cheaper, and perfect for Lambda proxy integrations like yours. REST APIs add features like API keys and per-client throttling that you do not need here.

  • For API name, enter ops-tickets-api.
  • For IP address type, keep IPv4 selected.
  • Click Next.
  • On the Configure routes page, click Next to skip route creation for now.
  • Review the $default stage that API Gateway creates for you, then click Next.
  • Click Create.

You should see your new ops-tickets-api listed in the API Gateway console.

Don't see the API created?

Make sure you are in the API Gateway service, not the Lambda console. Check the service name in the top-left corner of the console.

Help me find my API in the API Gateway console.

Now you need to create routes so API Gateway knows which requests to forward to your Lambda function.

  • Click on your ops-tickets-api to open it.
  • In the left sidebar, click Routes.
  • Click Create.
  • For Method, choose GET.
  • For the path, enter /tickets.
  • Click Create.

Your first route is created. Now add the second one.

  • Click Create again to add another route.
  • For Method, choose POST.
  • For the path, enter /tickets.
  • Click Create.

You should see both GET /tickets and POST /tickets listed on the Routes page.

What are routes?

A route is the combination of an HTTP method and a resource path. When a request comes in matching GET /tickets, API Gateway knows to forward it to the attached backend. Each route maps to one action your API can perform.

Next, you need to create an integration that connects these routes to your Lambda function.

  • In the left sidebar, click Integrations.
  • Click Manage integrations.
  • Click Create.
  • For Integration type, choose Lambda function.
  • For Lambda function, enter or select ops-ticket-handler.
  • Click Create.

The integration is created, but it is not attached to your routes yet. You need to attach it to each route individually.

  • On the Integrations page, click the GET /tickets route.
  • Under Choose an existing integration, select ops-ticket-handler.
  • Click Attach integration.

Now repeat the same attachment for the POST route.

  • Click the POST /tickets route.
  • Under Choose an existing integration, select ops-ticket-handler.
  • Click Attach integration.

Both routes should now show the Lambda integration attached.

Route not showing the Lambda integration?

Make sure you clicked Attach integration after selecting the function for each route. Each route needs to be attached individually.

Help me attach my Lambda integration to my routes.

Now grab the invoke URL so you can call your API from outside AWS.

  • In the left sidebar, click on your API name ops-tickets-api to go to the Details page.
  • Find the Invoke URL field and copy the URL. Record it here: your invoke URL

The URL follows the format https://{api-id}.execute-api.{region}.amazonaws.com. You will use it for every API call from here on.

Create a ticket from PowerShell

Your API is live on the internet. Time to prove it by creating a ticket from PowerShell, outside the AWS Console entirely.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Create a Critical priority ticket by running this command:
curl.exe -X POST -H "Content-Type: application/json" -d '{"title":"Server CPU High","description":"Web server CPU at 95%","priority":"Critical"}' [[INVOKE_URL="your invoke URL"]]/tickets

Why curl.exe instead of curl?

In PowerShell, curl is an alias for Invoke-WebRequest, which behaves differently from the real curl tool. Using curl.exe calls the actual curl binary that comes with Windows.

You should see a JSON response containing "message": "Ticket created" along with the ticket details, including a generated ticketId, the title Server CPU High, and a status of Open.

Seeing an error response?

If you see "Internal Server Error", check that your Lambda function's IAM role has DynamoDB permissions. Return to the Lambda console and verify ops-ticket-handler-role is attached.

If you see "Not Found", double check that your invoke URL is correct and includes /tickets at the end. Also verify that the POST /tickets route has the Lambda integration attached.

Help me debug my API Gateway error.

List all tickets from the live API

You created a ticket through the API. Now confirm it landed in DynamoDB alongside your earlier tickets.

  • Before you run the next command, predict: how many tickets do you expect to see in the response?
  • List all tickets by running this command:
curl.exe [[INVOKE_URL="your invoke URL"]]/tickets

You should see a JSON array containing all your tickets. This includes the TICKET-001 test ticket you created manually in Step 1, the ticket your Lambda test created in Step 2, and the Server CPU High ticket you just created through the API.

You now have a live REST API that anyone can call from any computer.

Missing tickets in the response?

Make sure the GET /tickets route has the Lambda integration attached in the API Gateway console. An unattached route returns an empty response or an error.

Help me troubleshoot my missing tickets.

Your API is live and proven. Next up, you will add the ability to update a ticket's status so you can move tickets through a real ops workflow.

Add the Ticket Lifecycle

Your API is live and you can create tickets and list them all from PowerShell. But what happens when someone starts working on a ticket? Right now, there is no way to update its status.

In a real ops team, tickets move through a lifecycle. A ticket starts as Open (just reported), shifts to In Progress (someone is on it), and ends at Resolved (fixed). Without this workflow, your ticketing system is just a static list.

In this step, get ready to:
  • Add two new route handlers to your Lambda function for retrieving and updating individual tickets.
  • Create matching routes in API Gateway so the new handlers are accessible from outside AWS.
  • Test the full ticket lifecycle from PowerShell: create, retrieve, update, and resolve.
Add the get-by-ID handler to your Lambda function

Your function currently handles two routes: POST /tickets and GET /tickets. You need two more. The first retrieves a single ticket by its ID so you can check its current status.

  • Return to the Lambda console and open your ops-ticket-handler function.
  • In the code editor, scroll down to the elif route_key == "GET /tickets": block.
  • Find the else: block that comes after it (the "Unsupported route" handler).
  • Add the following code between the GET /tickets block and the else: block:
        elif route_key == "GET /tickets/{ticketId}":
            # Extract the ticket ID from the URL path
            ticket_id = event["pathParameters"]["ticketId"]
            result = table.get_item(Key={"ticketId": ticket_id})
            if "Item" in result:
                body = result["Item"]
            else:
                # Return 404 if ticket doesn't exist
                status_code = 404
                body = {"error": f"Ticket {ticket_id} not found"}

What does this code do?

This handler reads the ticket ID from the URL path (e.g. /tickets/TICKET-001) using event["pathParameters"]. API Gateway automatically extracts path variables and passes them in this dictionary.

The get_item() call fetches exactly one item by its primary key. If the ticket exists, it returns the full item. If not, the function returns a 404 error so the caller knows the ID was invalid.

You will verify this handler works after adding the update handler next.

Add the update-status handler

The second handler lets you update a ticket's status. This is the core of the lifecycle workflow.

  • Right after the code you just added (still before the else: block), add the following handler:
        elif route_key == "PATCH /tickets/{ticketId}":
            ticket_id = event["pathParameters"]["ticketId"]
            request_body = json.loads(event.get("body", "{}"))
            new_status = request_body.get("status", "")
            # Only allow valid lifecycle transitions
            allowed_statuses = ["Open", "In Progress", "Resolved"]
            if new_status not in allowed_statuses:
                status_code = 400
                body = {"error": f"Invalid status. Allowed values: {allowed_statuses}"}
            else:
                now = datetime.now(timezone.utc).isoformat()
                table.update_item(
                    Key={"ticketId": ticket_id},
                    # 'status' is a DynamoDB reserved word, so use an alias
                    UpdateExpression="SET #s = :s, updatedAt = :u",
                    ExpressionAttributeNames={"#s": "status"},
                    ExpressionAttributeValues={":s": new_status, ":u": now},
                )
                body = {"message": f"Ticket {ticket_id} updated to {new_status}"}

What does this code do?

The PATCH handler reads the requested status from the request body and validates it against three allowed values: Open, In Progress, and Resolved. If the status is invalid, it returns a 400 error.

The update_item() call uses ExpressionAttributeNames to alias status as #s. This is required because status is a DynamoDB reserved word. Without the alias, the update would fail with a validation error.

  • Click Deploy to save your updated function code.

Now test the new get-by-ID handler directly from the Lambda console to confirm it works before adding the API Gateway routes.

  • Click the Test tab.
  • Under Test event, choose Create new event.
  • Enter GetTicketById for the Name.
  • Replace the default JSON with the following test event:
{
  "routeKey": "GET /tickets/{ticketId}",
  "pathParameters": {
    "ticketId": "TICKET-001"
  }
}
  • Click Test.
  • Under Execution result, expand Details. You should see a 200 status code and the full details of TICKET-001 (the test ticket you created manually in Step 1).

Seeing an error in the test result?

  • If you see a 404 response, the ticket ID TICKET-001 might not exist in your table. Open the DynamoDB console and check that the item is still there.
  • If you see a 500 or syntax error, make sure you clicked Deploy before clicking Test. Undeployed changes are not picked up.
  • Check that the new elif blocks sit between the GET /tickets handler and the else: block, not inside either one.

Help me debug my Lambda test result error.

✔️ Awesome, I've got everything!

Great. Make sure you clicked Deploy and your function is saved.

ⓧ I'd like to double check the full code

Your function should now have four route handlers in this order inside the try: block:

  • POST /tickets (creates a ticket).
  • GET /tickets (lists all tickets).
  • GET /tickets/{ticketId} (gets one ticket by ID).
  • PATCH /tickets/{ticketId} (updates a ticket's status).

After the four handlers, the else: block returns an "Unsupported route" error, and the except block catches unexpected exceptions. Scroll through your code and confirm each section is present in order.

Add the new routes in API Gateway

Your Lambda function can handle the new routes, but API Gateway does not know about them yet. You need to create two new routes and attach your existing Lambda integration to each one.

  • Return to the API Gateway console and select your ops-tickets-api.
  • In the left sidebar, click Routes.
  • Click Create.
  • For Method, choose GET.
  • For the path, enter /tickets/{ticketId}.
  • Click Create.

You should see the new GET /tickets/{ticketId} route in the list. Now create the second route.

  • Click Create again.
  • For Method, choose PATCH.
  • For the path, enter /tickets/{ticketId}.
  • Click Create.

Both routes exist, but they are not connected to your Lambda function yet. You need to attach the integration.

  • In the left sidebar, click Integrations.
  • Click the GET /tickets/{ticketId} route.
  • Under Choose an existing integration, select ops-ticket-handler.
  • Click Attach integration.
  • Repeat for the PATCH /tickets/{ticketId} route: click it, choose ops-ticket-handler, and click Attach integration.

All four routes should now show the Lambda integration attached. Your API auto-deploys to the $default stage, so the new routes are live immediately.

Routes not showing the integration?

Make sure you selected Integrations in the left sidebar (not Routes). The attach option only appears on the Integrations page.

Help me attach my Lambda integration to my API Gateway routes.

Test the full ticket lifecycle from PowerShell

Everything is wired up. Time to walk a ticket through its entire lifecycle from the command line: create it, look it up, update its status, and confirm the change.

  • Create a new ticket by running this command in PowerShell:
curl.exe -X POST -H "Content-Type: application/json" -d '{\"title\":\"Network Latency Spike\",\"description\":\"Latency exceeding 500ms on us-east-1\",\"priority\":\"High\"}' [[API_URL="your API invoke URL"]]/tickets

You should see a 200 response with a ticketId in the output (something like TICKET-A1B2C3D4).

  • Copy the ticketId value from the response. You will use it in the next commands.
  • Retrieve that ticket by ID by running:
curl.exe [[API_URL="your API invoke URL"]]/tickets/[[TICKET_ID="TICKET-XXXXXXXX from the create response"]]

You should see the full ticket details with "status": "Open".

Before you update this ticket, what do you think the response will look like when you change the status to In Progress? Will you see the full ticket, or just a confirmation message?

  • Update the ticket's status to In Progress by running:
curl.exe -X PATCH -H "Content-Type: application/json" -d '{\"status\":\"In Progress\"}' [[API_URL="your API invoke URL"]]/tickets/[[TICKET_ID="TICKET-XXXXXXXX from the create response"]]

You should see a confirmation message like "Ticket TICKET-XXXXXXXX updated to In Progress".

  • Verify the change stuck by retrieving the ticket again:
curl.exe [[API_URL="your API invoke URL"]]/tickets/[[TICKET_ID="TICKET-XXXXXXXX from the create response"]]

The status field should now show In Progress instead of Open. The updatedAt timestamp should also be more recent than createdAt.

  • Complete the lifecycle by resolving the ticket:
curl.exe -X PATCH -H "Content-Type: application/json" -d '{\"status\":\"Resolved\"}' [[API_URL="your API invoke URL"]]/tickets/[[TICKET_ID="TICKET-XXXXXXXX from the create response"]]

You should see "updated to Resolved" in the response. Your ticket has moved through the full ops lifecycle.

Getting a "Not Found" or empty response?

  • Make sure you attached the Lambda integration to both new routes in the API Gateway Integrations page. Routes without an integration return a generic error.
  • Double-check that you used curl.exe (not curl) in PowerShell. The bare curl command is aliased to a different tool that handles requests differently.
  • Verify the ticket ID you pasted matches exactly what the create response returned, including the TICKET- prefix.

Help me debug my curl request to the tickets API.

Your ticketing system now has a real workflow. Tickets can move through Open, In Progress, and Resolved just like a real incident management tool. Next up, you will see what happens when a critical ticket arrives and nobody gets notified.

See the Gap - No Alerting

Your ticketing system now handles the full incident lifecycle. You can create tickets, retrieve them by ID, and move them through Open, In Progress, and Resolved statuses.

But imagine you are on an ops team and a production database just went down. You create a Critical ticket. What happens? The same thing that happens when someone files a Low priority ticket about a minor UI glitch. Nothing distinguishes one from the other. In this step, you will experience that gap firsthand.

In this step, get ready to:
  • Create a Critical priority ticket through the live API.
  • Observe that Critical tickets receive no special treatment or alerting.
Create a Critical ticket

A production database is unreachable. This is a Critical incident. In a real ops center, someone needs to respond to this within minutes.

  • Switch back to PowerShell and create a Critical ticket by running this command:
curl.exe -X POST -H "Content-Type: application/json" -d '{\"title\":\"Database Down\",\"description\":\"Production database unreachable\",\"priority\":\"Critical\"}' [[API_URL="your API invoke URL"]]/tickets

The API returns a 200 response with the new ticket data. It looks exactly the same as any other ticket creation response.

What just happened?

You reported a production database outage with Critical priority. The API accepted it, stored it in DynamoDB, and returned a success message. No alarm went off. No email was sent. No one was paged.

List all tickets and spot the gap

Before you run the next command, think about this: if you were an on-call engineer looking at the ticket list, would anything in the output tell you that a production database is currently down and needs immediate attention?

  • List all tickets by running:
curl.exe [[API_URL="your API invoke URL"]]/tickets

Look at the output. Your Critical "Database Down" ticket sits right alongside every other ticket. There is no visual distinction, no urgency signal, and no notification.

Why does this matter?

In a real ops center, a Critical ticket means something is actively broken and customers are affected. If no one is alerted, that outage could sit unnoticed for hours.

Right now, your API treats a "Database Down" Critical ticket and a Low priority formatting request exactly the same way. The priority field is stored in the database, but nothing acts on it. This is the gap the Secret Mission solves.

Not seeing your Critical ticket in the list?

  • Make sure you used curl.exe (not curl) in PowerShell. The curl alias points to a different command that returns HTML instead of JSON.
  • Check that your invoke URL ends with /tickets and does not have a trailing slash after it.
  • If the POST command returned an error, check that the JSON body uses escaped quotes (\") inside the single-quoted string.

Help me troubleshoot why my curl command is not returning ticket data.

Secret mission

Alert on Critical Tickets with SNS

Critical tickets deserve immediate attention, but your API treats them the same as Low priority tickets. Wire up Amazon SNS email notifications so that creating a Critical ticket automatically triggers an alert in your inbox, just like a real on-call paging system.

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. Most resources in this project are covered by the AWS Always Free tier, so leaving them idle costs nothing.

Cost warning

Your API Gateway HTTP API (ops-tickets-api) is covered by the 12-month Free Tier, which includes 1,000,000 HTTP API calls per month. After your 12-month period ends, API calls are billed at $1.00 per million requests.

All other resources (DynamoDB, Lambda, SNS, IAM) are covered by the AWS Always Free tier with no time limit. If you are past the 12-month window, consider deleting the API Gateway API to avoid charges.

Resources you used:

  • DynamoDB table ops-tickets. Covered by the Always Free tier (25 GB storage, 25 RCU, 25 WCU). No ongoing cost.
  • Lambda function ops-ticket-handler. Covered by the Always Free tier (1,000,000 requests/month). No ongoing cost.
  • HTTP API ops-tickets-api in API Gateway. Covered by the 12-month Free Tier (1,000,000 calls/month). Costs apply after 12 months.
  • SNS topic critical-ticket-alerts with email subscription (from the Secret Mission). Covered by the Always Free tier. No ongoing cost.
  • IAM role ops-ticket-handler-role. No cost.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the project soon.

  • DynamoDB, Lambda, and SNS are all covered by the AWS Always Free tier. They only incur costs when actively used, and this project's usage stays well within the free allowances.
  • API Gateway is covered by the 12-month Free Tier. Monitor your free tier usage in the AWS Billing console if you are approaching the 12-month mark.
  • The IAM role has no cost regardless of whether it is active or idle.

Pause - I'll come back to this later

There is no pause option for these serverless resources. They only cost money when they receive requests, so leaving them idle is effectively free.

  • Your Lambda function, DynamoDB table, SNS topic, and IAM role sit idle at no cost until you use them again.
  • If you want to prevent any accidental usage, you can delete the API Gateway API (the public entry point) and recreate it later. The Lambda function and DynamoDB data will remain intact.

Delete - I don't want to use this again

Remove all project resources. Delete them in this order so that the public entry point is removed first.

API Gateway:

  • Open the API Gateway console and select APIs in the left sidebar.
  • Select ops-tickets-api.
  • Click Delete and confirm by typing the API name when prompted.

Lambda:

  • Open the Lambda console and select Functions in the left sidebar.
  • Select ops-ticket-handler.
  • Click Actions, then Delete function. Confirm the deletion.

DynamoDB:

  • Open the DynamoDB console and select Tables in the left sidebar.
  • Select the ops-tickets table.
  • Click Delete table. When prompted, confirm by selecting the option to delete all CloudWatch alarms for the table, then click Delete.

SNS (if you completed the Secret Mission):

  • Open the SNS console and select Topics in the left sidebar.
  • Select critical-ticket-alerts.
  • Click Delete and confirm. This also removes any subscriptions attached to the topic.

IAM:

  • Open the IAM console and select Roles in the left sidebar.
  • Search for ops-ticket-handler-role.
  • Select the role and click Delete. Confirm by typing the role name when prompted.

Nice Work!

Nice Work!

Nice work! You just built a live serverless API on AWS that manages cloud operations tickets the way real ops teams do.

You've learned how to:

  • Built a DynamoDB table and a Python Lambda function that creates, lists, retrieves, and updates operations tickets with priority levels and status validation.
  • Exposed a live REST API through API Gateway that you can call from any terminal, connecting three core AWS services in a real-world cloud operations pattern.
  • Implemented a ticket lifecycle workflow moving tickets through Open, In Progress, and Resolved statuses, mirroring how real incident management teams track and resolve issues.
  • Secret Mission: Wired up Amazon SNS email notifications so Critical tickets automatically trigger an alert when created.

Ready to quiz yourself?