Amazon DynamoDB¶
1. What is DynamoDB?¶
DynamoDB is a fully managed, serverless, multi-model NoSQL database built for internet-scale workloads requiring single-digit millisecond latency at any volume. You provision no servers, manage no OS, and write no patching scripts — only design your data model and interact via API.
Core trade-off:
RDS: SQL + ACID + complex queries → structured, relational data
DynamoDB: Flexible schema + speed → unstructured, high-volume, low latency
2. Core Data Model¶
Table
└── Item (one record — equivalent to a row)
├── Attribute: { "user_id": "123" } ← Partition Key (required)
├── Attribute: { "timestamp": "2026-…" } ← Sort Key (optional)
├── Attribute: { "name": "Ibtisam" }
└── Attribute: { "tags": ["aws","k8s"] } ← can differ per item (schema-less)
| Concept | RDS Equivalent | DynamoDB |
|---|---|---|
| Database | Database | Table |
| Row | Record | Item |
| Column | Field | Attribute |
| Primary Key | Primary Key | Partition Key (+ optional Sort Key) |
| Index | Index | GSI / LSI |
| Max row size | Varies | 400 KB per item |
3. Primary Key Types ⭐¶
Simple Primary Key (Partition Key Only)¶
Table: Users
PK: user_id
Items:
{ "user_id": "u-001", "name": "Ibtisam" }
{ "user_id": "u-002", "name": "Ali" }
Query: GetItem(user_id="u-001") → exact match only
Composite Primary Key (Partition Key + Sort Key)¶
Table: Orders
PK: user_id (partition key)
SK: order_date (sort key)
Items in same partition (same user):
{ "user_id": "u-001", "order_date": "2026-01-01", "amount": 99 }
{ "user_id": "u-001", "order_date": "2026-03-15", "amount": 150 }
{ "user_id": "u-001", "order_date": "2026-04-01", "amount": 200 }
Query: all orders for user u-001 in 2026
→ KeyConditionExpression: user_id = "u-001" AND order_date BETWEEN "2026-01-01" AND "2026-12-31"
Partition key determines physical placement (via hash function). Sort key determines order within the partition (sorted by value). Composite key enables range queries within a partition — essential design pattern.
4. Partitioning — How DynamoDB Scales ⭐¶
DynamoDB hashes partition key → determines which physical partition stores the item
Each partition: max 10 GB data, max 3,000 RCUs, max 1,000 WCUs
Table grows → DynamoDB splits partitions automatically (no downtime)
Hot Partition Problem ⭐¶
Bad design: status as partition key
"PENDING": 10,000 writes/sec → all hit one partition → throttled ❌
"COMPLETE": 100 writes/sec
"FAILED": 10 writes/sec
Good design: user_id or order_id as partition key (high cardinality)
user-001: 5 writes/sec
user-002: 3 writes/sec
user-003: 8 writes/sec
→ evenly distributed across partitions ✅
Partition key best practices:
- Use high cardinality keys: user IDs, order IDs, session IDs
- Avoid low cardinality: boolean flags, status codes, dates, country codes
- If forced to use low-cardinality key: add random suffix (
STATUS#PENDING#7) then scatter-gather query across all suffixes on read
5. Data Types¶
| Category | Types |
|---|---|
| Scalar | String (S), Number (N), Binary (B), Boolean (BOOL), Null (NULL) |
| Set | String Set (SS), Number Set (NS), Binary Set (BS) — all items same type, no duplicates |
| Document | List (L) — ordered, any types; Map (M) — key-value, any types |
{
"user_id": { "S": "u-001" },
"age": { "N": "25" },
"active": { "BOOL": true },
"tags": { "SS": ["aws", "k8s", "devops"] },
"address": {
"M": {
"city": { "S": "Rawalpindi" },
"zip": { "S": "46000" }
}
},
"scores": { "L": [{ "N": "95" }, { "N": "87" }] }
}
6. Capacity Modes ⭐¶
On-Demand (Pay Per Request)¶
No capacity planning — DynamoDB scales instantly
Pay: per actual read/write request
Use when: unpredictable traffic, new applications, spiky workloads
Cost: ~2.5× more expensive per request than provisioned
Provisioned (Fixed Capacity)¶
You set: RCUs (read capacity units) per second
WCUs (write capacity units) per second
Pay: per provisioned RCU/WCU per hour regardless of usage
Use when: predictable, steady workloads
Add: Auto Scaling to adjust provisioned capacity based on utilization
7. RCU / WCU Calculations ⭐¶
Write Capacity Unit (WCU)¶
1 WCU = 1 write per second for item up to 1 KB
Formula:
WCU = writes_per_second × CEIL(item_size_KB / 1 KB)
Examples:
Item = 0.5 KB, 100 writes/sec → 100 × CEIL(0.5) = 100 × 1 = 100 WCU
Item = 1.5 KB, 100 writes/sec → 100 × CEIL(1.5) = 100 × 2 = 200 WCU
Item = 3 KB, 50 writes/sec → 50 × CEIL(3) = 50 × 3 = 150 WCU
Read Capacity Unit (RCU)¶
1 RCU = 1 strongly consistent read per second for item up to 4 KB
OR
0.5 RCU = 1 eventually consistent read per second for item up to 4 KB
Formula:
RCU (strong) = reads_per_second × CEIL(item_size_KB / 4 KB)
RCU (eventual) = reads_per_second × CEIL(item_size_KB / 4 KB) × 0.5
Examples:
Item = 4 KB, 100 strongly consistent reads/sec → 100 × 1 = 100 RCU
Item = 4 KB, 100 eventually consistent reads/sec → 100 × 0.5 = 50 RCU
Item = 6 KB, 50 strongly consistent reads/sec → 50 × CEIL(6/4) = 50 × 2 = 100 RCU
Item = 10 KB, 80 eventually consistent reads/sec → 80 × CEIL(10/4) × 0.5 = 80 × 3 × 0.5 = 120 RCU
Transactional reads/writes consume 2× RCU/WCU — each transactional operation uses double the capacity of a standard operation.
8. Consistency Models ⭐¶
Default: Eventually Consistent (faster, costs 0.5 RCU)
→ Read may return data that is milliseconds behind
→ Use for: high-throughput read workloads, leaderboards, caches
Strongly Consistent (slower, costs 1 RCU)
→ Always returns latest committed data
→ Use for: financial balances, inventory, any "must be latest" data
→ Add --consistent-read flag in API call
Not available for:
→ Global Tables (always eventually consistent cross-region)
→ GSI reads (always eventually consistent)
9. Secondary Indexes ⭐¶
Local Secondary Index (LSI)¶
Same partition key as base table, DIFFERENT sort key
Must be created AT TABLE CREATION — cannot add later
Max: 5 LSIs per table
Storage: shares table storage
Reads: supports strongly consistent reads
Table: Orders (PK: user_id, SK: order_date)
LSI: Orders-by-amount (PK: user_id, SK: amount)
Query: "get all orders for user u-001 sorted by amount"
→ Use LSI (same partition key = user_id)
Global Secondary Index (GSI)¶
DIFFERENT partition key + optional different sort key
Can be added/deleted at ANY TIME (even after table creation)
Max: 20 GSIs per table
Storage: separate storage (own RCU/WCU)
Reads: eventually consistent only
Table: Orders (PK: user_id, SK: order_date)
GSI: Orders-by-status (PK: status, SK: order_date)
Query: "get all PENDING orders sorted by date"
→ Cannot do on base table (status not a key)
→ Use GSI (new partition = status) ✅
GSI Projection Types¶
| Type | What is copied to GSI | Storage cost |
|---|---|---|
KEYS_ONLY | Only PK + SK + GSI key | Lowest |
INCLUDE | Keys + specified attributes | Medium |
ALL | All attributes | Highest (= full item copy) |
LSI vs GSI Comparison¶
| Feature | LSI | GSI |
|---|---|---|
| Partition key | Same as table | Different |
| Create timing | At table creation only | Anytime |
| Strongly consistent reads | ✅ Yes | ❌ No (eventually consistent) |
| Storage | Shared with table | Separate |
| Capacity | Uses table RCU/WCU | Own RCU/WCU |
| Max count | 5 | 20 |
10. Query vs Scan ⭐¶
| Operation | Uses Index | Reads | Cost |
|---|---|---|---|
| GetItem | Exact PK lookup | Single item | Very cheap |
| Query | Partition key required | Items in one partition | Cheap |
| Scan | No index used | Entire table | ❌ Expensive |
| BatchGetItem | Up to 100 exact PK lookups | Multiple items | Cheap |
Scan is dangerous:
Table: 50 million items × 400 KB max = up to 20 TB scan
→ Consumes massive RCUs
→ Can throttle production traffic
→ Use FilterExpression to reduce returned data (but scan still reads ALL items)
Design principle:
"Design your table around your access patterns — never design and then query"
→ Know your queries first → then design PK/SK/GSI to serve those queries
Expression Types¶
| Expression | Purpose |
|---|---|
KeyConditionExpression | Filter by PK and SK (Query only) |
FilterExpression | Filter after reading (Scan or Query) |
ProjectionExpression | Return only specific attributes |
ConditionExpression | Conditional write — only write IF condition is true |
UpdateExpression | Define what to update in UpdateItem |
11. Transactions ⭐¶
DynamoDB supports full ACID transactions across multiple tables and items:
# All-or-nothing: both writes succeed or both fail
dynamodb.transact_write_items(
TransactItems=[
{
'Put': {
'TableName': 'Orders',
'Item': {'order_id': {'S': 'o-123'}, 'status': {'S': 'CONFIRMED'}}
}
},
{
'Update': {
'TableName': 'Inventory',
'Key': {'product_id': {'S': 'p-456'}},
'UpdateExpression': 'SET stock = stock - :val',
'ConditionExpression': 'stock >= :val',
'ExpressionAttributeValues': {':val': {'N': '1'}}
}
}
]
)
# If inventory check fails (stock < 1) → entire transaction rolls back
Cost: Transactions consume 2× RCU/WCU per operation vs standard. Limit: Up to 100 items or 4 MB per transaction.
12. DynamoDB Streams ⭐¶
Captures a time-ordered log of every change (INSERT / MODIFY / REMOVE) to items:
Insert/Update/Delete item
→ Event written to Stream (retention: 24 hours)
→ Lambda trigger reads Stream
→ Process event: audit log, replication, search index update, notifications
Stream record contains:
KEYS_ONLY: Only key attributes
NEW_IMAGE: New item state after change
OLD_IMAGE: Old item state before change
NEW_AND_OLD_IMAGES: Both states (most common — enables delta comparison)
Use cases: - Cross-region replication (foundation of Global Tables) - Triggers: send email on order status change - Search: sync changes to Elasticsearch/OpenSearch - Audit trail: every change logged immutably
13. DynamoDB Accelerator (DAX) ⭐¶
In-memory cache purpose-built for DynamoDB — reduces read latency from milliseconds to microseconds:
Without DAX:
App → DynamoDB API → 1–10ms latency per read
With DAX:
App → DAX Cluster (cache) → ~microseconds if cached
→ DynamoDB if cache miss (~1–10ms)
DAX is a cluster (primary + replicas) deployed in your VPC
→ Only accessible within VPC (not public)
→ Drop-in replacement: change endpoint in SDK, code stays same
Write-through cache:
Write → DAX → DynamoDB → both updated
→ Reads immediately see updated data from cache
| Property | Value |
|---|---|
| Latency | Microseconds (cache hit) |
| Access | VPC only (in-VPC cluster) |
| Use case | Read-heavy, repeated same item reads (product catalog, gaming) |
| Does NOT help | Write-heavy workloads, strongly consistent reads (bypasses cache) |
14. Global Tables ⭐¶
Active-active multi-Region replication — same table writable in multiple Regions:
Table: Users (Global Table)
us-east-1 replica: App writes user-001 ← write here
eu-west-1 replica: EU users read/write ← replicated from us-east-1
ap-southeast-1: APAC users read/write
Replication: DynamoDB Streams-powered, async, typically < 1 second
Active-active: any region can accept writes
→ Last-writer-wins for conflict resolution (based on timestamp)
Requirements:
- DynamoDB Streams enabled with
NEW_AND_OLD_IMAGES - All replicas: same table name, same partition key, same capacity settings
- Replicas must be empty when adding a new Region
Use case: Global apps where multi-region read AND write latency matters.
15. Backup and Restore¶
On-Demand Backup¶
Manual snapshot → full table backup
Stored indefinitely (until deleted)
Restores to new table (does not overwrite existing)
No RCU consumption during backup
Point-in-Time Recovery (PITR)¶
Enable PITR → DynamoDB continuously backs up table
Restore to any second in last 35 days
Restore creates new table — original table untouched
No performance impact on production table
16. TTL (Time To Live) ⭐¶
Automatically deletes expired items — no WCU consumed for deletions:
Add attribute: "expires_at": 1780000000 (Unix timestamp)
Enable TTL on table, point to "expires_at"
DynamoDB: periodically scans for expired items → deletes them within ~48 hours
Important:
- Not real-time — deletion can be up to 48 hours after expiry
- Expired-but-not-deleted items still returned in reads until deleted
- Add FilterExpression to exclude expired items if needed:
FilterExpression: "expires_at > :now"
Use cases: sessions, OTPs, cache, temporary tokens, rate limiting counters
17. DynamoDB Table Classes¶
| Class | Storage cost | Use Case |
|---|---|---|
| Standard | $0.25/GB/month | Frequently accessed data |
| Standard-Infrequent Access (IA) | $0.10/GB/month | Rarely accessed, large tables |
Standard-IA has higher read/write costs but 60% lower storage cost. Use for tables with large data but infrequent access (audit logs, historical data).
18. PartiQL — SQL-Like Interface¶
DynamoDB supports PartiQL — a SQL-compatible query language (SELECT/INSERT/UPDATE/DELETE):
-- PartiQL query
SELECT * FROM Orders WHERE user_id = 'u-001' AND order_date > '2026-01-01'
-- Still uses partition key internally — no full table scans without PK
-- Easier for developers who know SQL but want DynamoDB's scale
19. Access Control — Fine-Grained IAM¶
IAM conditions allow restricting access to specific items within a table:
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:us-east-1:123:table/UserData",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": ["${aws:userid}"]
}
}
}
This IAM policy only lets a user access items where the partition key equals their own user ID. Users cannot read each other's data. This is fine-grained access control — a key DynamoDB security feature.
20. DynamoDB vs RDS — When to Use Which ⭐¶
| Need | Use |
|---|---|
| Complex JOINs across multiple tables | RDS |
| Strict ACID transactions | RDS (or DynamoDB transactions for simpler cases) |
| Flexible schema, items differ per record | DynamoDB |
| Millions of requests/sec with ms latency | DynamoDB |
| Known, stable access patterns | DynamoDB (design table for patterns) |
| Ad-hoc queries and reporting | RDS |
| Serverless, no capacity planning | DynamoDB on-demand |
| Gaming leaderboards, session data, IoT | DynamoDB |
| Financial records, ERP, CRM | RDS |
21. Common Mistakes¶
| ❌ Wrong | ✅ Correct |
|---|---|
| DynamoDB uses SQL | DynamoDB uses API operations (GetItem, Query, Scan) + PartiQL as optional interface |
| Scan is equivalent to Query | Scan reads the entire table — extremely expensive at scale |
| GSI supports strongly consistent reads | GSI reads are always eventually consistent |
| LSI can be added after table creation | LSI must be created at table creation only |
| DAX caches all reads | DAX does NOT cache strongly consistent reads |
| TTL deletes items immediately | TTL deletion happens within up to 48 hours of expiry |
| Transactions are free | Transactions consume 2× RCU/WCU per operation |
| Any attribute can be partition key | Partition key must be high cardinality — low cardinality creates hot partitions |
| DynamoDB is eventually consistent always | Strongly consistent reads available (but cost 2× RCU and not for GSI/Global Tables) |
| Global Tables = read-only replicas | Global Tables are active-active — all replicas accept writes |
22. Interview Questions Checklist¶
- DynamoDB vs RDS — when do you choose each?
- What is a partition key? Why does cardinality matter?
- Simple vs composite primary key — example of when to use composite?
- Calculate RCU for: 6 KB item, 100 strongly consistent reads/sec
- Calculate WCU for: 3.5 KB item, 200 writes/sec
- Eventually consistent vs strongly consistent — cost difference, when to use
- LSI vs GSI — 5 differences (creation time, consistency, storage, PK, count)
- What is a hot partition? How do you fix it?
- Query vs Scan — why is Scan dangerous at scale?
- What are the five expression types in DynamoDB?
- Transactions — what do they cost in RCU/WCU? Max items?
- DynamoDB Streams — what does NEW_AND_OLD_IMAGES give you?
- DAX — what does it cache? What does it NOT cache?
- Global Tables — replication model? Conflict resolution? Requirements?
- TTL — when are items actually deleted?
- On-demand vs provisioned — when do you choose each?
- Fine-grained IAM — how do you restrict a user to their own items only?
- What is PITR? Retention window?
23. Creating a DynamoDB Table via AWS CLI ⭐¶
This section covers the complete workflow for creating a DynamoDB table from the CLI, creating an IAM policy scoped to that table, and binding it to a Kubernetes ServiceAccount via IRSA — the standard pattern for microservices running on EKS.
Key insight: DynamoDB is schemaless — declare only key attributes¶
Before looking at the flags, understand this critical difference from SQL databases:
SQL (RDS): You define ALL columns at table creation time.
DynamoDB: You declare ONLY the attributes used as keys (PK, SK, GSI keys).
All other attributes are written freely at runtime — no schema needed.
Result: --attribute-definitions never lists all your fields.
It only lists the fields that appear in --key-schema or --global-secondary-indexes.
Step 1: Create the DynamoDB Table¶
aws dynamodb create-table \
--table-name cart \
--attribute-definitions \
AttributeName=id,AttributeType=S \
AttributeName=customerId,AttributeType=S \
--key-schema \
AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--global-secondary-indexes '[
{
"IndexName": "idx_global_customerId",
"KeySchema": [
{ "AttributeName": "customerId", "KeyType": "HASH" }
],
"Projection": { "ProjectionType": "ALL" }
}
]'
Flag-by-flag explanation¶
--table-name cart
Names the table. This is the identifier your application SDK calls reference when performing GetItem, PutItem, Query, etc.
--attribute-definitions
AttributeName=id,AttributeType=S
AttributeName=customerId,AttributeType=S
Declares the type of every attribute that will be used as a key anywhere in the table — either as the primary key or as a GSI/LSI key. This is not a column definition. DynamoDB is schemaless; fields like items, quantity, createdAt are written at runtime and never declared here.
You must declare both id and customerId here because: - id is used in --key-schema as the table primary key - customerId is used in --global-secondary-indexes as the GSI partition key
AttributeType values:
| Code | Type |
|---|---|
S | String |
N | Number |
B | Binary |
--key-schema
AttributeName=id,KeyType=HASH
Defines the primary key of the table. KeyType=HASH means id is the partition key — DynamoDB hashes its value to determine which physical partition stores the item. Every item must have a unique id value.
There is no KeyType=RANGE (sort key) here, so id alone uniquely identifies each cart.
KeyType values:
HASH → Partition key (required, must be unique per item)
RANGE → Sort key (optional, enables range queries within a partition)
If a composite key were used (e.g., cart service that keeps history), the schema would be:
--key-schema \
AttributeName=customerId,KeyType=HASH \
AttributeName=createdAt,KeyType=RANGE
--billing-mode PAY_PER_REQUEST
Controls how read/write capacity is charged:
PAY_PER_REQUEST (On-Demand):
→ Pay per actual read/write operation
→ No capacity planning — DynamoDB scales automatically
→ Best for: variable traffic, new tables, unknown workload patterns
PROVISIONED:
→ You set fixed RCU/WCU per second
→ Pay per provisioned capacity regardless of actual usage
→ Best for: steady, predictable workloads
→ Can add Auto Scaling to adjust capacity automatically
For microservices projects with variable traffic, PAY_PER_REQUEST avoids throttling during traffic spikes and eliminates capacity planning.
--global-secondary-indexes
This flag is the most complex. A GSI creates an alternate index on the table that uses a different partition key, allowing queries that would otherwise require a full table Scan.
Base table primary key: id
→ App can look up a cart by its ID: GetItem(id="cart-001") ✅
→ App CANNOT find all carts for a customer without scanning the entire table ❌
GSI with customerId as partition key:
→ App can now query: "give me all carts where customerId = 'user-123'" ✅
→ DynamoDB maintains this index automatically as items are written
Breaking down each field in the GSI JSON:
{
"IndexName": "idx_global_customerId",
"KeySchema": [
{ "AttributeName": "customerId", "KeyType": "HASH" }
],
"Projection": { "ProjectionType": "ALL" }
}
| Field | Value | Meaning |
|---|---|---|
IndexName | idx_global_customerId | The name your application uses when querying this index. Must be unique per table. |
KeySchema.KeyType | HASH | customerId becomes the partition key of this index. |
Projection.ProjectionType | ALL | All item attributes are copied into the GSI. |
Why is it called Global? Because it uses a completely different partition key than the base table. A Local Secondary Index (LSI) must share the same partition key as the table but allows a different sort key — and must be created at table creation time. GSIs can be added or deleted at any time.
GSI Projection choices:
| Type | Attributes stored in index | Storage cost | Use case |
|---|---|---|---|
KEYS_ONLY | Only PK + SK + GSI key | Lowest | When you only need to confirm existence |
INCLUDE | Keys + named attributes | Medium | When you need a few specific fields |
ALL | All item attributes | Highest | When you need the full item from the GSI query |
ALL is used here so the Cart service can retrieve the complete cart data directly from the GSI query without a second GetItem call on the base table.
Step 2: Create the IAM Policy for DynamoDB Access¶
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
cat > cart-dynamo-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllAPIActionsOnCart",
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": [
"arn:aws:dynamodb:us-east-1:${ACCOUNT_ID}:table/cart",
"arn:aws:dynamodb:us-east-1:${ACCOUNT_ID}:table/cart/index/*"
]
}
]
}
EOF
aws iam create-policy \
--policy-name cart-dynamo \
--policy-document file://cart-dynamo-policy.json
Why two Resource ARNs?¶
IAM evaluates permissions at the resource level. DynamoDB tables and their indexes are separate IAM resource types:
arn:aws:dynamodb:us-east-1:ACCOUNT:table/cart
→ Covers table-level operations: PutItem, GetItem, DeleteItem, UpdateItem, Query, Scan
arn:aws:dynamodb:us-east-1:ACCOUNT:table/cart/index/*
→ Covers index-level operations: Query against any GSI/LSI on the carts table
Without the second ARN:
→ Querying idx_global_customerId returns AccessDeniedException
→ Even though the table permission exists, index access is denied separately
The /index/* wildcard covers all current and future indexes on the table, so adding a new GSI later does not require an IAM policy update.
Step 3: Bind IRSA to the Cart Namespace¶
IRSA (IAM Roles for Service Accounts) lets a Kubernetes ServiceAccount assume an IAM role via the cluster's OIDC provider. The Cart microservice pod uses this ServiceAccount to authenticate to DynamoDB without any hardcoded credentials.
eksctl create iamserviceaccount \
--cluster $CLUSTER_NAME \
--namespace cart \
--name cart \
--attach-policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/cart-dynamo \
--role-name dynamo-table-access-for-cart \
--approve \
--override-existing-serviceaccounts
| Flag | Purpose |
|---|---|
--cluster | Binds the IAM role trust policy to this cluster's OIDC provider. |
--namespace cart | The Kubernetes namespace where the ServiceAccount is created. |
--name cart | The name of the Kubernetes ServiceAccount. Must match what the Cart deployment references in serviceAccountName. |
--attach-policy-arn | Attaches the cart-dynamo policy to the new IAM role. |
--role-name | Explicit IAM role name for clarity and IAM console visibility. |
--approve | Applies immediately without a confirmation prompt. |
--override-existing-serviceaccounts | Updates the IRSA annotation if the ServiceAccount already exists instead of failing. |
After this command, the Cart ServiceAccount in kube-system will have the annotation:
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/dynamo-table-access-for-cart
The EKS pod identity webhook injects temporary AWS credentials into the pod via a projected token, and the AWS SDK in the Cart service picks them up automatically.
Step 4: Verify the ServiceAccount¶
kubectl get sa -n cart cart -o yaml
Confirm the annotation is present:
kubectl get sa -n cart cart -o yaml | grep role-arn
Expected output:
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/dynamo-table-access-for-cart
Access pattern summary for the carts table¶
Access pattern → How it is served
────────────────────────────────────────────────────────────
Get cart by ID → GetItem on base table (PK: id)
Get all carts for a customer → Query on GSI (PK: customerId)
Create / update a cart → PutItem / UpdateItem on base table
Delete a cart → DeleteItem on base table
The GSI exists precisely because the second access pattern (get all carts for a customer) cannot be served by the base table primary key. Without it, the only alternative would be a full Scan — unacceptable at production scale.