Το DynamoDB Local είναι το τοπικό υποκατάστατο του AWS DynamoDB από το 2013. Είναι ένα Java JAR, τρέχει στη μνήμη ή πάνω από ένα αρχείο SQLite, δέχεται σχεδόν οποιοδήποτε σχήμα αιτήματος, δεν έχει πραγματικό auth και αντιμετωπίζει τα Streams αρκετά χαλαρά. Είναι εντάξει για unit tests. Αρχίζει να ραγίζει τη στιγμή που ο κώδικάς σου κάνει κάτι πέρα από PutItem και GetItem.

Το ExtendDB v0.1.0 μόλις κυκλοφόρησε. Είναι μια clean-room υλοποίηση του πρωτοκόλλου DynamoDB wire, γραμμένο σε Rust από μηχανικούς της AWS, υποστηριζόμενο από PostgreSQL, Apache 2.0. Το pitch είναι "DynamoDB Local, αλλά μπορείς να το πάρεις στα σοβαρά." Αυτό το post είναι μια πρακτική ματιά στο αν αυτό ισχύει.

Τι είναι πραγματικά το ExtendDB

Το ExtendDB δεν είναι fork του DynamoDB. Δεν υπάρχει πηγαίος κώδικας DynamoDB σε αυτό. Αυτό που κάνει είναι να μιλά το πρωτόκολλο DynamoDB wire (το JSON-1.0 X-Amz-Target dispatch που χρησιμοποιούν τα AWS SDKs), οπότε ένα υπάρχον boto3 ή AWS CLI client μπορεί να στραφεί σε ένα ExtendDB endpoint με μία αλλαγή flag και λειτουργεί, χωρίς τροποποιήσεις.

Κάτω από το καπό είναι ένα μικρό, εστιασμένο Rust workspace:

flowchart LR
    bin[extenddb<br/>CLI + daemon] --> server[extenddb-server<br/>HTTP + console]
    server --> engine[extenddb-engine<br/>DynamoDB op handlers]
    server --> auth[extenddb-auth<br/>SigV4 + IAM policy]
    engine --> core[extenddb-core<br/>types, expressions]
    engine --> storage[extenddb-storage<br/>trait definitions]
    storage --> pg[extenddb-storage-postgres<br/>PostgreSQL backend]

Το extenddb-storage crate είναι απλά trait definitions. Το extenddb-storage-postgres είναι η μόνη υλοποίηση σήμερα. Το seam είναι αρκετά καθαρό ώστε ένα διαφορετικό backend (sqlite, foundationdb, οτιδήποτε) θα περιοριζόταν κυρίως στην υλοποίηση των traits. Ορίστε το lifecycle trait, αυτούσιο από το crates/storage/src/lib.rs:

/// All methods receive `account_id` to scope operations to a single account.
/// This enables multi-account isolation: different accounts can have tables
/// with the same name without conflict.
pub trait TableEngine: Send + Sync {
    fn create_table(
        &self,
        account_id: &str,
        input: CreateTableInput,
    ) -> BoxFuture<'_, Result<TableDescription, StorageError>>;
    fn delete_table(
        &self,
        account_id: &str,
        input: DeleteTableInput,
    ) -> BoxFuture<'_, Result<TableDescription, StorageError>>;
    fn describe_table(
        &self,
        account_id: &str,
        input: DescribeTableInput,
    ) -> BoxFuture<'_, Result<TableDescription, StorageError>>;
    // ...list_tables, update_table, table_key_info
}

Δύο πράγματα αξίζει να παρατηρήσεις. Πρώτον, κάθε μέθοδος δέχεται ένα account_id. Το account scoping δεν είναι ένα layer πάνω από το storage, είναι περασμένο σε όλη τη διαδρομή. Δεύτερον, το trait χρησιμοποιεί BoxFuture για object safety.

Lab setup

Και τα δύο backends τρέχουν στο ίδιο Mac. Postgres 18.4 από Homebrew, εκκινημένο με brew services start postgresql@18. Το ExtendDB χτίστηκε με cargo build --release, μετά:

$ ./target/release/extenddb init --config extenddb.toml
…δημιουργία catalog database extenddb_catalog…
…δημιουργία self-signed TLS πιστοποιητικού στο ~/.extenddb/tls/cert.pem…
  Admin credentials (εμφανίζονται μία φορά, αποθήκευσέ τα τώρα)
  │  Username: admin
  │  Password: <redacted>

Το TLS είναι υποχρεωτικό. Η εντολή init δημιουργεί ένα self-signed EC P-256 πιστοποιητικό 587-byte. Οι clients το εμπιστεύονται μέσω AWS_CA_BUNDLE=~/.extenddb/tls/cert.pem.

Το ExtendDB τρέχει με extenddb serve στο https://127.0.0.1:8000. Τα test credentials παρέχονται μέσω του management API από το bundled devtools/provision-test-credentials script, το οποίο δημιουργεί έναν test λογαριασμό, IAM user, access key και ένα allow-all-dynamodb policy.

Το DynamoDB Local ανεβαίνει δίπλα του σε Docker:

$ docker run -d --name ddb-local -p 8001:8000 \
    amazon/dynamodb-local:latest \
    -jar DynamoDBLocal.jar -inMemory -port 8000

Ένα boto3 conftest fixture εναλλάσσεται μεταξύ των δύο endpoints βάσει του LAB_BACKEND env var, οπότε κάθε probe τρέχει εναντίον οποιουδήποτε backend χωρίς αλλαγές κώδικα.

Baseline parity: Το CRUD λειτουργεί μια χαρά

Το πρώτο πράγμα που πρέπει να ξέρεις είναι ότι τα εύκολα πράγματα απλά λειτουργούν. Ένα PutItem με όλο τον ζωολογικό κήπο τύπων:

ddb.put_item(TableName="lab_alpha", Item={
    "pk":         {"S": "user#1"},
    "sk":         {"S": "profile"},
    "name":       {"S": "Alice"},
    "age":        {"N": "30"},
    "active":     {"BOOL": True},
    "tags":       {"L": [{"S": "rust"}, {"S": "postgres"}]},
    "meta":       {"M": {"city": {"S": "Istanbul"}}},
    "deleted_at": {"NULL": True},
    "blob":       {"B": b"\x00\x01\x02"},
})

Ίδια κλήση, και τα δύο backends, ίδια μορφή απόκρισης. Δέκα probes που καλύπτουν CreateTable (με GSI), strong-consistent GetItem, Query σε partition + sort range, Query στο GSI, Scan με filter, UpdateItem με συνδυασμένα SET/REMOVE/ADD συν ένα ConditionExpression, DeleteItem με condition, BatchWriteItem 25 αντικειμένων και ένα TransactWriteItems με σκόπιμη σύγκρουση condition-check. Όλα περνούν και στα δύο backends.

Μια μικρή εξαίρεση που αξίζει να σημειωθεί. Η απόκριση BatchWriteItem του ExtendDB παραλείπει το κλειδί UnprocessedItems εντελώς όταν το batch πέτυχε πλήρως. Το πραγματικό DynamoDB και το DynamoDB Local επιστρέφουν και τα δύο "UnprocessedItems": {}.

Πού αποκλίνουν

Το auth είναι πραγματικό, κάπως

Και τα δύο backends απορρίπτουν ένα εντελώς ανυπόγραφο αίτημα:

# B1: κανένα Authorization header σε κανένα
extenddb         -> 400 com.amazon.coral.service#MissingAuthenticationToken
dynamodb-local   -> 400 com.amazonaws.dynamodb.v20120810#MissingAuthenticationToken

Το πραγματικό κενό εμφανίζεται στο B2. Στείλε οποιοδήποτε Authorization header που κάνει parse ως SigV4 και:

# B2: ψεύτικο αλλά αναλύσιμο Authorization header
extenddb         -> 400 UnrecognizedClientException
                       "The security token included in the request is invalid."
dynamodb-local   -> 200 {"TableNames":[]}

Το ExtendDB αναζητά το access key στο IAM store του και απορρίπτει όταν δεν υπάρχει. Το DynamoDB Local δεν κοιτάζει καν το access key. Αποδέχεται το αίτημα τη στιγμή που το header κάνει parse.

Streams διαμορφωμένα σαν το πραγματικό

Και τα δύο backends υλοποιούν Streams. Το σχήμα είναι διαφορετικό:

# Αφελής ανάγνωση single-shard εναντίον του ExtendDB επιστρέφει μηδέν εγγραφές.
# Το ExtendDB καταμερίζει τις εγγραφές stream σε 4 shards με βάση το partition-key hash.
records = []
for shard in describe_stream(stream_arn)["StreamDescription"]["Shards"]:
    it = get_shard_iterator(StreamArn=stream_arn,
                            ShardId=shard["ShardId"],
                            ShardIteratorType="TRIM_HORIZON")["ShardIterator"]
    # ...άδειασμα σελίδων μέχρι να αδειάσει

Αυτό ταιριάζει με το πώς το πραγματικό DynamoDB καταμερίζει τα streams. Το DynamoDB Local ακολουθεί μια απλούστερη διαδρομή και βάζει τα πάντα σε ένα μόνο shard.

TTL με πραγματικές εγγραφές REMOVE

Το ExtendDB τρέχει έναν TTL sweeper:

ddb.put_item(TableName="lab_ttl_...",
             Item={"pk": {"S": "doomed"},
                   "expires_at": {"N": str(int(time.time()) - 60)},
                   "payload": {"S": "x"}})
# 60 δευτερόλεπτα αργότερα
ddb.get_item(...)  # κανένα Item

Το DynamoDB Local παραλείπει όλη αυτή την ιστορία: καθόλου sweeper, τα ληγμένα αντικείμενα μένουν στον πίνακα για πάντα.

Multi-account isolation που λειτουργεί

Δημιουργήθηκε δεύτερος λογαριασμός lab_b. Και οι δύο δημιούργησαν έναν πίνακα με το ίδιο όνομα lab_shared_name:

Account A reads back  -> {"owner": "from-a", "pk": "x"}
Account B reads back  -> {"owner": "from-b", "pk": "x"}

Ίδιο όνομα πίνακα, διαφορετικός λογαριασμός, ξεχωριστές γραμμές, καμία διασταύρωση. Το DynamoDB Local έχει ακριβώς ένα namespace.

Μπορείς να κοιτάξεις μέσα

Το ExtendDB αποθηκεύει τα αντικείμενα σε απλή PostgreSQL:

$ psql -d extenddb -c "SELECT * FROM _ddb_f6453e99-3173-... LIMIT 2;"
   pk    |                                  item_data
---------+------------------------------------------------------------------------------
 item-001| {"pk": {"S": "item-001"}, "owner": {"S": "account-a"}, "value": {"N": "42"}}
 item-002| {"pk": {"S": "item-002"}, "owner": {"S": "account-a"}, "value": {"N": "99"}}

Για backup, restore, replication, debugging ή απλά ικανοποίηση περιέργειας, αυτός είναι ένας διαφορετικός κόσμος από το αδιαφανές in-memory ή SQLite αρχείο του DynamoDB Local.

Πότε ΝΑ ΜΗΝ το χρησιμοποιήσεις

  • Δεν έχει υλοποιηθεί καθόλου: Global Tables, DAX, PartiQL, federated SAML/OIDC auth, Kinesis streaming destinations.
  • Διαφορετικό σχήμα: Τα ImportTable / ExportTableToPointInTime πηγαίνουν σε τοπική διαδρομή filesystem αντί για S3 bucket.
  • Wire-protocol drift: BatchWriteItem παραλείπει το UnprocessedItems: {}. Τα πεδία userIdentity χρησιμοποιούν PascalCase.
  • v0.1.0 bug: ένας stream-enabled πίνακας δεν μπορεί να διαγραφεί και να ξαναδημιουργηθεί με το ίδιο όνομα στον ίδιο κύκλο ζωής διακομιστή.

Ετυμηγορία

Αν έχεις γράψει ποτέ κώδικα δοκιμών που κάλυπτε το DynamoDB Local (mocked auth, παρέλειψε Streams assertions, έκανε stub το TTL) και ανησυχούσες σιωπηλά ότι η πραγματική υπηρεσία θα συμπεριφερόταν διαφορετικά, το ExtendDB είναι ενδιαφέρον. Είναι το πρώτο τοπικό υποκατάστατο DynamoDB που παίρνει στα σοβαρά το auth, τα Streams, το TTL, το multi-account isolation και την ανθεκτικότητα, και το storage layer είναι απλή PostgreSQL στην οποία μπορείς να κάνεις psql.

Για CI pipelines που χρειάζονται ρεαλιστική σημασιολογία auth και stream, on-prem ή air-gapped deployments και ομάδες ανάπτυξης που έχουν χάσει χρόνο με το "καλά, δουλεύει στο DynamoDB Local...", το v0.1.0 είναι ήδη ένα χρήσιμο εργαλείο.

Links: