RDS

Real PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, and Db2 instances via Docker. Snapshots, read replicas, parameter groups.

fakecloud implements 163 of 163 RDS operations at 100% Smithy conformance. DB instances run in real Docker containers — your code connects to a real database, not a mock.

Supported features

  • DB instances — CreateDBInstance, ModifyDBInstance, DeleteDBInstance, DescribeDBInstances, RebootDBInstance
  • Real engines via Docker — PostgreSQL, MySQL, MariaDB, Oracle (gvenzl/oracle-free), SQL Server (mssql/server Express), Db2 (db2_community)
  • Snapshots — automated and manual, CreateDBSnapshot, RestoreDBInstanceFromDBSnapshot, CopyDBSnapshot, DeleteDBSnapshot
  • Read replicas — CreateDBInstanceReadReplica, PromoteReadReplica
  • Parameter groups — DBParameterGroup and DBClusterParameterGroup CRUD, parameter management
  • Option groups — CRUD
  • Subnet groups — CRUD
  • DB clusters — Aurora-style clusters with full lifecycle: ModifyDBCluster (every mutable field on AWS's surface, including ServerlessV2 scaling, log-export updates, VPC SGs, NewDBClusterIdentifier rename), StartDBCluster / StopDBCluster (status transitions with InvalidDBClusterStateFault validation), RebootDBCluster, FailoverDBCluster (auto-picks a replica when no target is provided, swaps the writer flag, rejects non-member targets when the cluster tracks members), BacktrackDBCluster (Aurora MySQL only — InvalidParameterCombination on Aurora PostgreSQL — records BacktrackTo and the change-record count, append-tracked under DescribeDBClusterBacktracks)
  • Events — DescribeEvents, DescribeEventCategories, DescribeEventSubscriptions
  • Engine discovery — DescribeDBEngineVersions with real engine metadata
  • Tagging — AddTagsToResource, RemoveTagsFromResource
  • Dump and restore — MySQL and MariaDB database dumps for snapshot/restore flows
  • License models — tracking
  • EventBridge events — lifecycle ops emit aws.rds events on the default bus, deliverable to SQS, SNS, Lambda, etc. via standard EB rules
  • PostgreSQL aws_lambda extension — call fakecloud Lambda functions from inside RDS PostgreSQL via CREATE EXTENSION aws_lambda CASCADE and aws_lambda.invoke(...) (subset of the AWS RDS extension surface; see below)
  • PostgreSQL aws_s3 extension — import objects from fakecloud S3 into tables (aws_s3.table_import_from_s3) and export query results back to S3 (aws_s3.query_export_to_s3); see below
  • MySQL / MariaDB Aurora Lambda bridge — Aurora-compatible mysql.lambda_async / mysql.lambda_sync stored procedures invoke fakecloud Lambda functions from inside the DB container; see below

EventBridge integration

Lifecycle ops emit events matching the AWS event schema (source: "aws.rds", detail-type per source kind):

OperationEventIDSource typeCategories
CreateDBInstanceRDS-EVENT-0005DB_INSTANCEcreation
DeleteDBInstanceRDS-EVENT-0003DB_INSTANCEdeletion
ModifyDBInstanceRDS-EVENT-0014DB_INSTANCEconfiguration change
RebootDBInstanceRDS-EVENT-0006DB_INSTANCEavailability
StartDBInstanceRDS-EVENT-0088DB_INSTANCEnotification
StopDBInstanceRDS-EVENT-0089DB_INSTANCEnotification
CreateDBInstanceReadReplicaRDS-EVENT-0005DB_INSTANCEcreation, read replica
RestoreDBInstanceFromDBSnapshotRDS-EVENT-0043DB_INSTANCEcreation
CreateDBSnapshotRDS-EVENT-0042DB_SNAPSHOTcreation
DeleteDBSnapshotRDS-EVENT-0041DB_SNAPSHOTdeletion
ModifyDBClusterRDS-EVENT-0016DB_CLUSTERconfiguration change
RebootDBClusterRDS-EVENT-0006DB_CLUSTERnotification
StartDBClusterRDS-EVENT-0150DB_CLUSTERnotification
StopDBClusterRDS-EVENT-0151DB_CLUSTERnotification
FailoverDBClusterRDS-EVENT-0072DB_CLUSTERfailover
BacktrackDBClusterRDS-EVENT-0095DB_CLUSTERnotification

Match with an EventBridge rule pattern like:

{ "source": ["aws.rds"], "detail-type": ["RDS DB Instance Event"] }

Protocol

Query protocol. Form-encoded body, Action parameter, XML responses.

Introspection

  • GET /_fakecloud/rds/instances — list fakecloud-managed DB instances with runtime metadata (container id, host port)
  • POST /_fakecloud/rds/lambda-invoke — internal bridge used by the PostgreSQL aws_lambda extension to invoke fakecloud Lambda functions from inside the DB container
  • POST /_fakecloud/rds/s3-import / POST /_fakecloud/rds/s3-export — internal bridges used by the PostgreSQL aws_s3 extension to read/write fakecloud S3 objects from inside the DB container

PostgreSQL aws_lambda extension

Matches the AWS RDS extension of the same name. Lets SQL running inside an RDS-managed PostgreSQL instance invoke fakecloud Lambda functions:

CREATE EXTENSION IF NOT EXISTS aws_lambda CASCADE;

SELECT aws_commons.create_lambda_function_arn('my_function');

SELECT * FROM aws_lambda.invoke(
    'my_function',
    '{"body":"Hello!"}'::json
);

Implemented function signatures (subset of the AWS RDS Lambda API):

  • aws_lambda.invoke(function_name text, payload json, region text DEFAULT NULL, invocation_type text DEFAULT 'RequestResponse') -> returns (status_code int, payload json, executed_version text, log_result text)
  • aws_lambda.invoke(function_name aws_commons._lambda_function_arn_1, payload json, region text DEFAULT NULL, invocation_type text DEFAULT 'RequestResponse') (composite-typed overload)
  • aws_commons.create_lambda_function_arn(function_name text, region text DEFAULT NULL) -> composite of (function_name, region)

invocation_type = 'Event' returns (202, NULL, '$LATEST', NULL) immediately and runs the Lambda asynchronously.

The first time you create a PostgreSQL DB instance, fakecloud lazily builds a fakecloud-postgres:<major>-<hash> Docker image off postgres:<major> that bakes in plpython3u and the extension files. The build typically takes ~60s and the image is cached locally for subsequent runs (the hash invalidates the cache when fakecloud changes the embedded extension definitions).

Inside the container, the extension's plpython3u body POSTs to http://host.docker.internal:<server_port>/_fakecloud/rds/lambda-invoke, which routes through fakecloud's standard Lambda invocation path.

PostgreSQL aws_s3 extension

Matches the AWS RDS extension of the same name. Lets SQL running inside an RDS-managed PostgreSQL instance read objects from fakecloud S3 directly into tables and write query results back as objects:

CREATE EXTENSION IF NOT EXISTS aws_s3 CASCADE;

-- Import a CSV file into a table
SELECT * FROM aws_s3.table_import_from_s3(
    'people',
    '',                           -- column list (empty = all columns)
    'format csv',                 -- COPY options
    aws_commons.create_s3_uri('my-bucket', 'people.csv', 'us-east-1')
);

-- Export a query result back to S3
SELECT * FROM aws_s3.query_export_to_s3(
    'SELECT id, name FROM people ORDER BY id',
    aws_commons.create_s3_uri('my-bucket', 'export.csv', 'us-east-1'),
    'format csv'
);

Implemented function signatures (subset of the AWS RDS S3 import/export API):

  • aws_s3.table_import_from_s3(table_name text, column_list text, options text, bucket text, file_path text, region text DEFAULT NULL) -> returns (rows_imported bigint, file_compression text, bytes_processed bigint)
  • aws_s3.table_import_from_s3(table_name text, column_list text, options text, s3_info aws_commons._s3_uri_1) (composite-typed overload)
  • aws_s3.query_export_to_s3(query text, bucket text, file_path text, region text DEFAULT NULL, options text DEFAULT NULL) -> returns (rows_uploaded bigint, files_uploaded bigint, bytes_uploaded bigint)
  • aws_s3.query_export_to_s3(query text, s3_info aws_commons._s3_uri_1, options text DEFAULT NULL) (composite-typed overload)
  • aws_commons.create_s3_uri(bucket text, file_path text, region text DEFAULT NULL) -> composite of (bucket, file_path, region)

The options argument is forwarded verbatim into the underlying postgres COPY WITH (...) clause (format csv, header true, delimiter ',', etc.). file_compression is always returned empty — fakecloud does not autodetect compression on import; pre-decompress objects before calling.

The bridges (/_fakecloud/rds/s3-import, /_fakecloud/rds/s3-export) read and write the in-memory S3 state of the same fakecloud server, so any object that's visible to a GetObject/PutObject call against fakecloud is reachable from aws_s3.

MySQL / MariaDB Aurora Lambda bridge

Aurora MySQL exposes Lambda invocation as built-in stored procedures (mysql.lambda_async, mysql.lambda_sync). fakecloud's prebuilt fakecloud-mysql and fakecloud-mariadb images provide the same surface so SQL inside an RDS-managed instance can invoke fakecloud Lambda functions:

-- Async, fire-and-forget. Returns immediately.
CALL mysql.lambda_async('my_function', '{"k":1}');

-- Synchronous, returns the function payload as a JSON string.
SELECT mysql.lambda_sync('my_function', '{"hello":"world"}');

Implemented procedures (subset of the AWS Aurora MySQL Lambda surface):

  • mysql.lambda_async(function_name TEXT, payload TEXT)Event-style invocation; returns nothing.
  • mysql.lambda_sync(function_name TEXT, payload TEXT) RETURNS TEXTRequestResponse; returns the function payload as JSON.

Under the hood the prebuilt image bakes a small libcurl-backed UDF (fakecloud_post, fakecloud_post_async) that POSTs to /_fakecloud/rds/lambda-invoke against host.docker.internal. A bootstrap script renders the host endpoint, account ID, and region from the container's FAKECLOUD_* env vars (set automatically by RdsRuntime) so SQL never has to know the host. Like the postgres image, the runtime tries to pull the published fakecloud-mysql:<major>-<fakecloud-version> (or fakecloud-mariadb:<major>-<fakecloud-version>) tag first and falls back to a local build when the pull fails.

Asynchronous instance creation

CreateDBInstance returns ~immediately with DBInstanceStatus = "creating". The container start (and the underlying image pull/build for postgres) runs in the background; DescribeDBInstances returns the live status. Callers should poll until the status flips to available before connecting:

import boto3
import time

rds = boto3.client("rds", endpoint_url="http://localhost:4566")
rds.create_db_instance(
    DBInstanceIdentifier="my-db",
    Engine="postgres",
    EngineVersion="16.3",
    MasterUsername="admin",
    MasterUserPassword="secret123",
    AllocatedStorage=20,
    DBInstanceClass="db.t3.micro",
)

while True:
    desc = rds.describe_db_instances(DBInstanceIdentifier="my-db")
    if desc["DBInstances"][0]["DBInstanceStatus"] == "available":
        break
    time.sleep(1)

This matches AWS RDS behavior — real CreateDBInstance also never blocks on the container coming up. The Endpoint element is omitted from create/describe responses while the instance is still in creating.

How the Docker integration works

When you call CreateDBInstance for PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2, fakecloud starts a real Docker container running the upstream image for that engine and version, waits for it to be ready, and reports the mapped host port. Your application connects to that port like it would connect to any database.

DeleteDBInstance stops and removes the container. RebootDBInstance restarts it. Snapshots serialize the DB state so it can be restored into a fresh container.

Engine -> image map

EngineImagePortWait probe
postgresghcr.io/faiscadev/fakecloud-postgres:<major>-<fakecloud-version> (prebuilt with plpython3u + the aws_commons, aws_lambda, and aws_s3 extensions on top of postgres:<major>; falls back to a local build if the pull fails)5432tokio-postgres ping
mysqlghcr.io/faiscadev/fakecloud-mysql:<major>-<fakecloud-version> (prebuilt with the libcurl-backed fakecloud_post UDF + Aurora-compatible mysql.lambda_async / mysql.lambda_sync stored procedures on top of mysql:<major>; falls back to a local build if the pull fails)3306mysql_async ping
mariadbghcr.io/faiscadev/fakecloud-mariadb:<major>-<fakecloud-version> (same UDF + stored procedures, on top of mariadb:<major>)3306mysql_async ping
oracle-ee / oracle-se2 (+-cdb)gvenzl/oracle-free:23-slim1521log marker DATABASE IS READY TO USE! + TCP probe
sqlserver-ee / -se / -ex / -webmcr.microsoft.com/mssql/server:2022-latest1433log marker SQL Server is now ready for client connections + TCP probe
db2-se / db2-aeicr.io/db2_community/db2:latest50000log marker Setup has completed + TCP probe

The Oracle / SQL Server / Db2 images are large (1-3 GB) and take 30-300 s to first-boot. fakecloud passes the engine-specific license-acceptance environment variables (ACCEPT_EULA, LICENSE) automatically. Db2 launches with --privileged because the container needs it to set kernel parameters during startup.

Prebuilt PostgreSQL image

ghcr.io/faiscadev/fakecloud-postgres:<major>-<fakecloud-version> is published on every fakecloud release tag (workflow: .github/workflows/docker-rds-images.yml) for postgres 13, 14, 15, 16, 17, both linux/amd64 and linux/arm64. Each release also gets a rolling :<major> tag pointing at the latest version for that major. Resolution order at runtime:

  1. Image already on the local Docker daemon -> use it.
  2. docker pull of the version-pinned tag -> use it.
  3. Local docker build from the embedded crates/fakecloud-rds/assets/postgres/ Dockerfile (covers dev / unreleased / airgapped setups).

Override knobs (env vars, both optional):

  • FAKECLOUD_POSTGRES_REGISTRY=registry.example.com/team — point at a private mirror (default ghcr.io/faiscadev).
  • FAKECLOUD_REBUILD_POSTGRES_IMAGE=1 — skip inspect + pull and force a fresh local build. Use after editing the embedded Dockerfile or extension SQL during development.

Describe filters

The Filters parameter is honored on the Describe operations in the table below. DescribePendingMaintenanceActions accepts the parameter and returns the unfiltered list. Filters are AND-ed with each other and with the operation's own identifier parameter; the values inside one filter are OR-ed. Filter names are case-sensitive and wildcards are not supported (same as AWS); values are case-sensitive too, except for the four enum-valued endpoint and backtrack filters noted below.

OperationSupported filter names
DescribeDBInstancesdb-cluster-id, db-instance-id, dbi-resource-id, domain, engine
DescribeDBSnapshotsdb-instance-id, db-snapshot-id, dbi-resource-id, engine, snapshot-type
DescribeDBClustersclone-group-id, db-cluster-id, db-cluster-resource-id, domain, engine
DescribeDBClusterSnapshotsdb-cluster-id, db-cluster-snapshot-id, engine, snapshot-type
DescribeDBClusterEndpointsdb-cluster-endpoint-id, db-cluster-endpoint-type, db-cluster-endpoint-custom-type, db-cluster-endpoint-status
DescribeDBClusterBacktracksdb-cluster-backtrack-id, db-cluster-backtrack-status
DescribeDBShardGroupsdb-shard-group-id

DescribeDBClusterBacktracks returns the backtracks BacktrackDBCluster recorded, scoped to the cluster named in the request; an unknown cluster raises DBClusterNotFoundFault. AWS marks DBClusterIdentifier required, but the only errors the RDS model declares on this operation are DBClusterNotFoundFault and DBClusterBacktrackNotFoundFault, so omitting it selects no cluster's backtracks rather than emitting an error shape that isn't in the model. Backtrack Status is reported lowercase (completed), matching the values the db-cluster-backtrack-status filter documents.

DescribeDBClusterEndpoints reports each cluster's built-in writer and reader endpoints alongside the custom ones, as AWS does. They belong to the cluster rather than the endpoint store -- there is no API that creates or deletes them -- so db-cluster-endpoint-type=reader matches them while custom matches the ones you created. They report no DBClusterEndpointIdentifier, DBClusterEndpointResourceIdentifier or DBClusterEndpointArn, matching AWS: a built-in has no identity of its own, and borrowing the cluster's would make a lookup by endpoint identifier return rows AWS doesn't. Their Status is mapped into the endpoint's own enum (creating, available, deleting, inactive, modifying), so a cluster that is backing up or upgrading reports modifying rather than a value that enum has no room for.

DeleteDBClusterEndpoint raises DBClusterEndpointNotFoundFault for an identifier that doesn't exist instead of reporting success, and reports the endpoint as deleting. DeleteDBCluster deletes the cluster's custom endpoints and backtrack records with it, as AWS does, so recreating a cluster under the same name doesn't inherit them.

CreateDBClusterEndpoint raises DBClusterEndpointAlreadyExistsFault for an identifier that already exists rather than overwriting it.

CreateDBClusterEndpoint only ever creates custom endpoints, so the request's EndpointType (READER, WRITER, ANY) is reported back as CustomEndpointType with EndpointType set to CUSTOM, matching AWS -- CustomEndpointType is an output member, not something a caller sends. ModifyDBClusterEndpoint retargets it the same way. StaticMembers and ExcludedMembers are persisted and reported too, along with the endpoint's ARN and resource id, so a caller can read back what it set and db-cluster-endpoint-custom-type has something to match.

The model documents no filter names for DescribeDBShardGroups; the shard group's own id is accepted, as on the sibling Describes. An identifier parameter that names another account's resource, or an ARN of the wrong resource type, matches nothing rather than falling back to the unfiltered list. A named DBShardGroupIdentifier or BacktrackIdentifier that doesn't exist raises the fault the model declares (DBShardGroupNotFound, DBClusterBacktrackNotFoundFault -- the first drops the shape name's Fault suffix on the wire, as AWS does) rather than an empty list, so a poller can tell "gone" from "no rows".

db-cluster-endpoint-type, db-cluster-endpoint-custom-type, db-cluster-endpoint-status and db-cluster-backtrack-status match case-insensitively. AWS returns the endpoint types uppercase (CUSTOM, READER) while documenting the filter values lowercase (custom, reader), so an exact comparison would return nothing for a caller copying the documented command. The status values are lowercase on both sides; matching them case-insensitively too just keeps the four filters consistent.

These three operations honor MaxRecords and Marker and report a Marker when more rows remain.

Listings come out in a stable order, so two identical requests report the same rows in the same order and a pagination marker stays meaningful between calls. It is identifier order for most listings; on DescribeDBClusterEndpoints a cluster's built-in endpoints sort under the cluster's own identifier, interleaved with the custom endpoints rather than ahead of them.

Filters documented as accepting "identifiers and ARNs" (db-instance-id, db-cluster-id, db-snapshot-id, db-cluster-snapshot-id) match either form.

# Only the instance with this resource id -- what the Terraform/OpenTofu
# AWS provider uses to read a DB instance back after creating it.
aws rds describe-db-instances \
  --endpoint-url http://localhost:4566 \
  --filters "Name=dbi-resource-id,Values=db-1b8e439e59564e849b38b04191c40b8e"

DescribeDBSnapshots honors SnapshotType including shared and public, which report snapshots another account shared with the caller (or with everyone) through ModifyDBSnapshotAttribute's restore attribute; IncludeShared / IncludePublic add those to an otherwise-unqualified listing. DescribeDBClusterSnapshots honors the same shared / public values, backed by ModifyDBClusterSnapshotAttribute, and the top-level SnapshotType parameter, and DescribeDBClusterSnapshots honors DBClusterSnapshotIdentifier / DBClusterIdentifier. A named resource that doesn't exist raises the operation's declared fault (DBInstanceNotFound, DBSnapshotNotFound, DBClusterNotFoundFault, DBClusterSnapshotNotFoundFault) rather than returning an empty list, so a client can tell "gone" from "no match".

Identifier parameters accept an ARN as well as a plain id (DBInstanceIdentifier, DBSnapshotIdentifier, DBClusterIdentifier, DBClusterSnapshotIdentifier, and the SnapshotIdentifier on the restore operations), matching what the Terraform provider stores in snapshot_identifier.

Reaching a snapshot another account shared with you takes the ARN, which names its owner. A plain id resolves a shared snapshot only while exactly one account shares one under that name; once two do, the id is ambiguous and DescribeDBSnapshots / DescribeDBClusterSnapshots raise the not-found fault rather than picking one of them, so a lookup can't quietly return the wrong account's snapshot or two rows for one name.

clone-group-id is populated by RestoreDBClusterToPointInTime with RestoreType=copy-on-write, which puts the clone and its source in one clone group; a full-copy restore is an independent cluster and carries no group.

ModifyDBSnapshotAttribute and ModifyDBClusterSnapshotAttribute resolve a value listed in both ValuesToAdd and ValuesToRemove by removing it (AWS rejects the request outright, but that error shape isn't in the RDS model). The response echoes the resulting attribute set, so a caller can confirm what was actually applied.

Real RDS rejects a filter name an operation doesn't support with InvalidParameterValue. fakecloud can't: that error isn't declared on any of these operations in the Smithy model, so returning it would emit an undeclared error shape. An unrecognized filter name matches no resource instead, so a caller that asked to narrow gets an empty result rather than the full list. Because nothing on the wire explains that, the name is logged at warn level.

CopyDBSnapshot records the source it copied from and reports it as SourceDBSnapshotIdentifier (an ARN, as AWS does); a snapshot that isn't a copy omits the field. That ARN is also what db-instance-id matches a copy's source instance against: a copy's own ARN names the account that made the copy, while the instance it records still belongs to the original owner, so for a cross-account or cross-region copy only the source ARN says where that instance lives. db-cluster-id does the same through SourceDBClusterSnapshotArn.

IAM role associations

AddRoleToDBCluster, RemoveRoleFromDBCluster, AddRoleToDBInstance and RemoveRoleFromDBInstance record the association and report it back as AssociatedRoles on DescribeDBClusters / DescribeDBInstances, with Status: ACTIVE. They previously accepted any request and answered 200 without storing anything, so a role could be attached to a cluster that did not exist and the listing never showed it.

An association is keyed on the (role, feature) pair, as AWS keys it, so one role can be attached for both s3Import and s3Export and removing one feature leaves the other. Roles are capped at five per cluster and per instance.

The faults the model declares are raised: the resource not existing (DBClusterNotFoundFault / DBInstanceNotFound), attaching a pair that is already attached (DBClusterRoleAlreadyExists / DBInstanceRoleAlreadyExists), removing one that is not (DBClusterRoleNotFound / DBInstanceRoleNotFound), and exceeding the cap (DBClusterRoleQuotaExceeded / DBInstanceRoleQuotaExceeded).

A restored cluster carries no associations: neither RestoreDBClusterFromSnapshot nor RestoreDBClusterToPointInTime takes roles, and the snapshot holds a clone of the source cluster.

Gotchas

  • Persistence snapshots are one-way across the v3 bump. RDS state written by this version declares snapshot schema v3 (final snapshots are typed manual, matching AWS; earlier builds wrote automated). An older fakecloud refuses to load a v3 file and exits, so downgrade by removing <data-path>/rds/snapshot.json first.

  • Requires a Docker socket. RDS needs access to /var/run/docker.sock to start and stop containers.

  • First use pulls the image. Expect a slower first run while the database image downloads. Heavy engines (Oracle/SQL Server/Db2) can pull 1-3 GB on first use. The PostgreSQL image is custom (ghcr.io/faiscadev/fakecloud-postgres:<major>-<version>) and is pulled from the registry when available; otherwise it's built locally (~60 s).

  • Aurora is partially supported. Aurora-specific features (Global Database, Serverless v2, I/O-optimized) are recorded but don't affect the real container.

  • Db2 needs --privileged. fakecloud sets it automatically; the host must allow privileged containers.

  • Heavy-engine boot is slow. Oracle takes 30-90 s to first-boot, Db2 30-60 s, SQL Server ~30 s. Factor this into test budgets.

Source