You add a todo · EC2 + RDS MySQL

11 steps from your browser to the database and back.

InternetAWS Region · us-east-1VPCPublic subnet · to IGWPrivate subnet · no internetYour browseron the internetAPI GatewayWebSocket API · live lineCloudWatch Logsapp log linesInternet gatewaythe VPC's doorEC2 instanceruns the web appRDS MySQLdatabase: todoioThe live line is already open1You add a todo2Into the VPC3Through the firewall4Who are you?5Ask the database6The database answers7Tell your other tabs8It shows up instantly9Back to you10Leave a note11

Text version of this diagram.

What sits where:

  • AWS Region · us-east-1: A Region is a part of the world where AWS has data centers. Most things you build live in one Region. us-east-1 is in northern Virginia. contains:
    • VPC: Virtual private cloud: your own private network inside AWS. Nothing gets in or out except the ways you allow. contains:
      • Public subnet · to IGW: A slice of the VPC that can reach the internet directly, through the internet gateway (IGW). contains:
        • EC2 instance (runs the web app): A virtual server you rent from AWS. You pick its size and operating system, and you look after it.
      • Private subnet · no internet: A slice of the VPC with no route to the internet at all. Nothing here can be reached from outside. contains:
        • RDS MySQL (database: todoio): A MySQL database that AWS runs for you, including the server, patches and backups. todoio is the database's name.
      • Internet gateway (the VPC's door): The door between a VPC and the internet. Without one, nothing in the VPC can talk to the internet directly.
    • API Gateway (WebSocket API · live line): The front door for the app's web requests. A WebSocket API keeps a connection open, so the server can push updates to your browser instantly.
    • CloudWatch Logs (app log lines): Collects the lines your app writes about what it's doing (its logs), so you can search them later.
  • Your browser (on the internet)

Steps, in order:

  1. The live line is already open. Path: Your browser → API Gateway. When you logged in, your browser opened a WebSocket to API Gateway. It stays open, so AWS can send you messages any time.
  2. You add a todo. Path: Your browser. Your browser sends POST /todos over HTTPS.
  3. Into the VPC. Path: Your browser → Internet gateway. todo.io points at the instance's public IP, so the request comes in through the VPC's internet gateway. The public IP changes if the instance is stopped and started (an Elastic IP stays put), so the DNS record has to follow it. In Level 2 todo.io points at a load balancer's DNS name instead, which never changes.
  4. Through the firewall. Path: Internet gateway → EC2 instance. The security group around the instance allows HTTPS on port 443, so the request reaches the web server. Checks: security-group: todo-app-sg: TCP 443 from 0.0.0.0/0 → allow.
  5. Who are you?. Path: EC2 instance. The app checks your signed session cookie and finds your user id, u-123. This is the app's own login, not AWS.
  6. Ask the database. Path: EC2 instance → RDS MySQL. The app sends SQL to RDS MySQL in the private subnet on port 3306. It logs in with a database username and password, not with IAM. Checks: security-group: todo-db-sg: TCP 3306 from todo-app-sg → allow; mysql: MySQL user todo_app: INSERT on todoio.todos → allow.
  7. The database answers. Path: RDS MySQL → EC2 instance. MySQL says: 1 row inserted, new id 42.
  8. Tell your other tabs. Path: EC2 instance → Internet gateway → API Gateway. The app looks up your connection id in the database and posts a message to it through the WebSocket API. Checks: iam: todo-app-ec2-role: execute-api:ManageConnections → allow.
  9. It shows up instantly. Path: API Gateway → Your browser. Every Todo.io tab you have open, on any device, shows the change without a refresh.
  10. Back to you. Path: EC2 instance → Internet gateway → Your browser. You get 201 Created, and "Buy milk" appears in your list.
  11. Leave a note. Path: EC2 instance → Internet gateway → CloudWatch Logs. The app's log line goes to CloudWatch Logs so Todo.io's engineers can see what happened. Like SQS, it's a public AWS service, so it leaves through the internet gateway.
Step 1 of 11
Step 1 of 11
  1. When you logged in, your browser opened a WebSocket to API Gateway. It stays open, so AWS can send you messages any time.

    Show me the real thing

    The WebSocket handshake

    GET /prod HTTP/1.1
    Host: ws123.execute-api.us-east-1.amazonaws.com
    Upgrade: websocket
    Connection: Upgrade
    
    → API Gateway assigns connection id Ab3xYzEXAMPLE=

    Exam tipAn API Gateway WebSocket API has $connect, $disconnect and $default routes. Each one can call Lambda or an HTTP backend.

  2. Your browser sends POST /todos over HTTPS.

    Show me the real thing

    The HTTP request

    POST /todos HTTP/1.1
    Host: todo.io
    Cookie: session=eyJ1c2VySWQiOiJ1LTEyMyJ9.Zm9v
    Content-Type: application/json
    
    {"title":"Buy milk"}
  3. todo.io points at the instance's public IP, so the request comes in through the VPC's internet gateway. The public IP changes if the instance is stopped and started (an Elastic IP stays put), so the DNS record has to follow it. In Level 2 todo.io points at a load balancer's DNS name instead, which never changes.

    Exam tipA subnet is "public" only because its route table sends 0.0.0.0/0 to an internet gateway. Nothing else makes it public.

  4. The security group around the instance allows HTTPS on port 443, so the request reaches the web server.

    • 🛡Security group todo-app-sg: TCP 443 from 0.0.0.0/0Allowed
    Show me the real thing

    Inbound rules on todo-app-sg

    Type    Protocol  Port  Source
    HTTPS   TCP       443   0.0.0.0/0

    Exam tipThe classic way to do HTTPS is a free AWS Certificate Manager (ACM) certificate on a load balancer or CloudFront (Level 2). Since 2025 ACM can also sell you an exportable certificate to install on the instance or container itself.

  5. The app checks your signed session cookie and finds your user id, u-123. This is the app's own login, not AWS.

  6. The app sends SQL to RDS MySQL in the private subnet on port 3306. It logs in with a database username and password, not with IAM.

    • 🛡Security group todo-db-sg: TCP 3306 from todo-app-sgAllowed
    • 🗄MySQL MySQL user todo_app: INSERT on todoio.todosAllowed
    Show me the real thing

    The SQL (real code sends the values as parameters, never pasted into the string)

    INSERT INTO todos (user_id, title, done) VALUES ('u-123', 'Buy milk', false);

    Exam tipA security group rule can use another security group as its source: "anything wearing todo-app-sg may connect". Exam favourite.

  7. MySQL says: 1 row inserted, new id 42.

  8. The app looks up your connection id in the database and posts a message to it through the WebSocket API.

    • 🔑IAM todo-app-ec2-role: execute-api:ManageConnectionsAllowed
    Show me the real thing

    Posting to a connection

    POST /prod/@connections/Ab3xYzEXAMPLE= HTTP/1.1
    Host: ws123.execute-api.us-east-1.amazonaws.com
    
    {"type":"todo.created","todoId":"42"}

    Exam tipYour browser never asked for this. Anything with permission can push to a connection id through the @connections API.

  9. Every Todo.io tab you have open, on any device, shows the change without a refresh.

  10. You get 201 Created, and "Buy milk" appears in your list.

    Show me the real thing

    The HTTP response

    HTTP/1.1 201 Created

    Exam tipSecurity groups are stateful: a reply to an allowed request goes back out automatically. No outbound rule needed for it.

  11. The app's log line goes to CloudWatch Logs so Todo.io's engineers can see what happened. Like SQS, it's a public AWS service, so it leaves through the internet gateway.

    Show me the real thing

    The log line

    2026-10-11T18:42:07Z INFO POST /todos user=u-123 status=201 14ms

    Exam tipEC2 doesn't send logs by itself. You install the CloudWatch agent and give the instance role permission to write logs.

What to notice

  • You run the server. Patching, scaling and the HTTPS certificate are your job. One instance in one Availability Zone is a single point of failure (Level 2 adds a load balancer and a second AZ).
  • IAM never checked your SQL. MySQL has its own users; IAM only manages the database server itself.
  • The database sits in a private subnet with no route to the internet. Only the security groups you allowed can reach it.
  • The database password lives in the app config here. In production it belongs in Secrets Manager (Level 1).
  • A WebSocket stays open, so the server side can send you updates without you asking.
  • You, the customer, were never an IAM user. IAM controls the app, not its customers.