Claude Code et AWS DynamoDB : clés, écritures sûres, TTL et coûts
Guide pratique Claude Code et DynamoDB : clés, tests locaux, TTL, IAM, coûts et partitions chaudes.
Demander à Claude Code « ajoute DynamoDB » est trop vague pour une application sérieuse. DynamoDB semble souple parce que chaque item peut avoir ses propres attributs, mais le vrai schéma se trouve dans les accès : quelle page lit quelles données, avec quelle clé, quelles écritures doivent être protégées, et comment la charge se répartit sur les clés de partition.
Ce guide montre comment utiliser Claude Code comme assistant de conception DynamoDB. Nous allons couvrir le design de table, les partition keys, le choix entre design simple et single-table design, les écritures conditionnelles, TTL, les tests locaux, IAM, les coûts et les erreurs de hot partition. Pour relier cette implémentation au reste d’AWS, consultez aussi le guide AWS Lambda, le guide AWS IAM et le guide CloudWatch.
Gardez les documents AWS officiels ouverts : data modeling foundations, partition key best practices, Query key condition expressions, condition expressions, TTL, DynamoDB local, throughput capacity et IAM fine-grained access control.
Commencer par les accès
Un pattern d’accès est une opération réelle : lister les tâches d’un projet, terminer une tâche, expirer une session après sept jours ou traiter un webhook une seule fois. Sans cette liste, Claude Code générera souvent du CRUD générique, puis ajoutera Scan ou des index pour compenser.
Avant le code, utilisez un prompt de cadrage :
Relis ce design DynamoDB avant d'écrire le code.
Besoins:
- Lister les tâches par projet
- Mettre à jour une tâche par taskId
- Expirer les sessions utilisateur après 7 jours
- Traiter chaque webhook eventId une seule fois
Retour attendu:
1. Tableau des patterns d'accès
2. Proposition PK/SK
3. Opérations possibles avec Query et impossibles
4. Écritures conditionnelles nécessaires
5. Risques de partition chaude et de coût
DynamoDB Query exige une égalité sur la partition key et peut ensuite filtrer avec la sort key. FilterExpression ne remplace pas un WHERE SQL pour économiser la lecture : la capacité est déjà consommée. Si Claude Code propose Scan pour une liste affichée à l’utilisateur, considérez-le comme un défaut de design.
Design simple ou single-table
Le single-table design place plusieurs types d’entités dans une seule table, avec des clés génériques comme PK et SK. Il est puissant lorsqu’un écran lit des entités liées ensemble, mais il rend plus délicats IAM, Streams, les sauvegardes et l’évolution des clés.
Le design simple sépare davantage les responsabilités. Pour un MVP ou un outil interne, il est souvent plus clair et plus facile à auditer.
| Critère | Design simple | Single-table design |
|---|---|---|
| Début de projet | Facile à expliquer | Revue plus stricte |
| Écran multi-entités | Plusieurs lectures possibles | Souvent un seul Query |
| IAM | Séparation par table | LeadingKeys critique |
| Évolution | Ajouter une table reste simple | Les noms de clés doivent tenir |
| Usage idéal | MVP, apprentissage, outils | Domaine stable et très lu |
L’exemple utilise une seule table, mais un périmètre réduit : projets, tâches, sessions et déduplication de webhooks.
ClaudeCodeLabDemo
PK SK entityType
PROJECT#alpha META Project
PROJECT#alpha TASK#task-001 Task
USER#u-001 SESSION#s-001 Session
WEBHOOK#stripe EVENT#evt_001 WebhookEvent
Requêtes:
- Tâches du projet: PK = PROJECT#alpha AND begins_with(SK, TASK#)
- Sessions utilisateur: PK = USER#u-001 AND begins_with(SK, SESSION#)
- Déduplication webhook: PutItem conditionnel sur la même PK/SK
Cas d’usage concrets
Premier cas : un tableau de tâches. Avec PK = PROJECT#projectId et SK = TASK#taskId, la liste du projet devient un Query. Si un filtre par statut devient essentiel, demandez à Claude Code de justifier un GSI ou de montrer pourquoi le tri côté application suffit.
Deuxième cas : sessions, invitations et jetons temporaires. TTL stocke une date d’expiration sous forme de Number en secondes Unix epoch. C’est utile pour nettoyer les données temporaires, mais ce n’est pas un planificateur exact. Un item expiré peut rester lisible avant suppression, donc l’application doit aussi vérifier expiresAt.
Troisième cas : l’idempotence des webhooks. Un fournisseur peut renvoyer le même événement. Avec WEBHOOK#provider, EVENT#eventId et attribute_not_exists(PK) AND attribute_not_exists(SK), seul le premier traitement réussit.
Quatrième cas : un rate limit simple. Une clé comme PK = RATE#userId et SK = WINDOW#2026-06-03T10:00 fonctionne pour des APIs modestes, mais peut devenir une partition chaude si un utilisateur ou un tenant concentre trop de trafic.
Setup local exécutable
Démarrez DynamoDB Local :
services:
dynamodb-local:
image: "amazon/dynamodb-local:latest"
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
ports:
- "8000:8000"
volumes:
- "./docker/dynamodb:/home/dynamodblocal/data"
working_dir: /home/dynamodblocal
docker compose up -d
export AWS_ACCESS_KEY_ID=fakeMyKeyId
export AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
export AWS_REGION=us-west-2
Créez la table locale :
aws dynamodb create-table \
--table-name ClaudeCodeLabDemo \
--attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S \
--key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--endpoint-url http://localhost:8000 \
--region us-west-2
Activez l’attribut TTL :
aws dynamodb update-time-to-live \
--table-name ClaudeCodeLabDemo \
--time-to-live-specification "Enabled=true,AttributeName=expiresAt" \
--endpoint-url http://localhost:8000 \
--region us-west-2
Installez les dépendances :
npm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
Implémentation copiable
Créez app.mjs. Ce script crée une tâche, liste les tâches du projet, termine une tâche avec condition et crée une session avec TTL.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
PutCommand,
QueryCommand,
UpdateCommand,
} from "@aws-sdk/lib-dynamodb";
const TABLE_NAME = process.env.TABLE_NAME ?? "ClaudeCodeLabDemo";
const isLocal = process.env.DDB_LOCAL !== "0";
const client = new DynamoDBClient({
region: process.env.AWS_REGION ?? "us-west-2",
...(isLocal
? {
endpoint: "http://localhost:8000",
credentials: {
accessKeyId: "fakeMyKeyId",
secretAccessKey: "fakeSecretAccessKey",
},
}
: {}),
});
const ddb = DynamoDBDocumentClient.from(client, {
marshallOptions: { removeUndefinedValues: true },
});
const nowIso = () => new Date().toISOString();
const ttlAfterDays = (days) => Math.floor(Date.now() / 1000) + days * 86400;
const taskKey = (projectId, taskId) => ({
PK: `PROJECT#${projectId}`,
SK: `TASK#${taskId}`,
});
async function createTask({ projectId, taskId, title, ownerId }) {
const item = {
...taskKey(projectId, taskId),
entityType: "Task",
title,
ownerId,
status: "OPEN",
createdAt: nowIso(),
updatedAt: nowIso(),
};
await ddb.send(
new PutCommand({
TableName: TABLE_NAME,
Item: item,
ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
}),
);
return item;
}
async function listProjectTasks(projectId) {
const result = await ddb.send(
new QueryCommand({
TableName: TABLE_NAME,
KeyConditionExpression: "PK = :pk AND begins_with(SK, :taskPrefix)",
ExpressionAttributeValues: {
":pk": `PROJECT#${projectId}`,
":taskPrefix": "TASK#",
},
ReturnConsumedCapacity: "TOTAL",
}),
);
console.log("consumed capacity:", result.ConsumedCapacity);
return result.Items ?? [];
}
async function completeTask({ projectId, taskId, expectedOwnerId }) {
const result = await ddb.send(
new UpdateCommand({
TableName: TABLE_NAME,
Key: taskKey(projectId, taskId),
UpdateExpression: "SET #status = :done, updatedAt = :now",
ConditionExpression: "ownerId = :ownerId AND #status <> :done",
ExpressionAttributeNames: {
"#status": "status",
},
ExpressionAttributeValues: {
":done": "DONE",
":ownerId": expectedOwnerId,
":now": nowIso(),
},
ReturnValues: "ALL_NEW",
}),
);
return result.Attributes;
}
async function createSession({ userId, sessionId }) {
await ddb.send(
new PutCommand({
TableName: TABLE_NAME,
Item: {
PK: `USER#${userId}`,
SK: `SESSION#${sessionId}`,
entityType: "Session",
createdAt: nowIso(),
expiresAt: ttlAfterDays(7),
},
ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
}),
);
}
async function main() {
const projectId = "alpha";
const taskId = `task-${Date.now()}`;
await createTask({
projectId,
taskId,
title: "Review DynamoDB key design",
ownerId: "masa",
});
await createSession({
userId: "masa",
sessionId: `session-${Date.now()}`,
});
console.log(await listProjectTasks(projectId));
console.log(
await completeTask({
projectId,
taskId,
expectedOwnerId: "masa",
}),
);
}
main().catch((error) => {
if (error.name === "ConditionalCheckFailedException") {
console.error("Condition failed:", error.message);
process.exit(2);
}
console.error(error);
process.exit(1);
});
DDB_LOCAL=1 node app.mjs
Prompt de refactorisation :
Transforme app.mjs en handler Lambda.
Ne supprime pas ConditionExpression.
N'ajoute pas Scan sans expliquer pourquoi Query ne convient pas.
Garde expiresAt comme Number en secondes Unix epoch.
Garde ReturnConsumedCapacity en développement.
IAM, coût et pièges
Dans une table partagée, l’accès à toute la table est souvent trop large. dynamodb:LeadingKeys permet de limiter par clé de partition.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProjectScopedDynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query"
],
"Resource": [
"arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd",
"arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd/index/*"
],
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": [
"PROJECT#${aws:PrincipalTag/projectId}"
]
}
}
}
]
}
Les pièges classiques sont connus : pages de liste en Scan, FilterExpression utilisé comme SQL, clés fixes comme GLOBAL, TTL compris comme suppression instantanée, GSI ajoutés sans pattern d’accès, écritures non conditionnelles pour commandes ou webhooks, endpoint local laissé en production.
Audite cette implémentation DynamoDB pour dépendance à Scan, partitions chaudes, mauvaise compréhension de TTL, écritures conditionnelles manquantes, IAM trop large et pics de coût on-demand. Donne d'abord les constats, puis les corrections.
Pour réutiliser ces prompts et checklists, consultez les produits ClaudeCodeLab. Pour une adoption en équipe avec AWS, IAM, CI et validation de production, passez par la formation et consultation Claude Code.
Conclusion
DynamoDB devient fiable quand les accès, les clés, les conditions d’échec, TTL, IAM et les signaux de coût sont explicites. Claude Code accélère la transformation en code, mais la conception doit rester vérifiée.
Note terrain (実際に試した結果) : le meilleur enchaînement a été de demander d’abord le tableau des patterns d’accès, puis de revoir PK/SK et les cas d’échec, et enfin d’exécuter les écritures conditionnelles sur DynamoDB Local. L’exemple webhook avec attribute_not_exists donne une règle de revue simple et très concrète.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.