DocsAWS 101Blog
← Back to Blog

AWS IoT Core, Athena ↔ Glue, EventBridge → Step Functions

May 18, 2026 · v1.3.43

One headline service (IoT Core), one big quality-of-life upgrade for analytics (Athena now reads Glue tables and persists results to S3), and a real EventBridge integration gap closed (Step Functions targets).

AWS IoT Core (Phase 1)

MiniStack now emulates AWS IoT Core end-to-end for the unblock-your-frontend-and-Lambda use case: backend services publish via the HTTP iot-data Publish API, browser clients subscribe over MQTT-over-WebSocket with the official SDK and a SigV4-signed upgrade, and a Local Certificate Authority issues client certificates on demand.

Control plane covers the things you'd actually declare in IaC or hit from a backend: Thing / ThingType / ThingGroup CRUD, certificates (CreateKeysAndCertificate, RegisterCertificate, UpdateCertificate, DeleteCertificate, ListCertificates), AttachThingPrincipal / DetachThingPrincipal with ListThingPrincipals / ListPrincipalThings back-references, IoT Policies with versioning + attach/detach, and DescribeEndpoint returning a per-account hostname.

Data plane is MQTT 3.1.1 over WebSocket on the gateway port — connect with the mqtt Sec-WebSocket-Protocol header and you're in. Persistent sessions (cleanSession=0), QoS 1 with in-flight tracking and DUP-flag retransmits, Last Will and Testament on ungraceful disconnect, duplicate-client-id force-disconnect, and retained-message delivery on subscribe are all implemented. HTTP publishes (POST /topics/{topic} with ?qos= + ?retain=) cross the bridge and fan out to subscribers identically.

import boto3, json

iot      = boto3.client("iot",      endpoint_url="http://localhost:4566")
iot_data = boto3.client("iot-data", endpoint_url="http://localhost:4566")

# Issue a client cert from the Local CA
cert = iot.create_keys_and_certificate(setAsActive=True)
print(cert["certificateArn"], cert["certificatePem"][:30], "...")

# Backend publishes via HTTP
iot_data.publish(
    topic="devices/abc/telemetry",
    payload=json.dumps({"temperature": 22.4}).encode(),
    qos=1,
)

The Local CA root certificate is exposed at GET /_ministack/iot/ca.pem so test code can configure SDK trust. The CA, issued certificates, retained messages, and persistent sessions all survive container restart when PERSIST_STATE=1.

Multi-tenancy is enforced by transparent topic prefixing at the bridge layer: the SigV4 access-key-derived account ID is captured at WebSocket upgrade, and every PUBLISH/SUBSCRIBE topic is internally prefixed with the account before it hits the in-process pub/sub registry — two accounts publishing to the same topic name never see each other's traffic.

Requires the cryptography package, which is declared as part of the [full] optional dependency. Slim image users who never call IoT pay zero — the module is lazy-loaded only on first IoT request — and if they do call without [full] installed they get a clean RuntimeError.

Deferred to later phases: Device Shadows, mTLS on port 8883, retained-message queries (ListRetainedMessages), Rules Engine, Jobs, Fleet Provisioning. IoT policy documents are stored but not enforced on the data plane. Plain unencrypted MQTT on TCP 1883 is intentionally not exposed — real AWS IoT Core requires TLS or SigV4 on every connection.

Athena now reads Glue tables and writes results back to S3

Two upgrades land together. First, StartQueryExecution resolves database.table references in the query against Glue's GetTable, picks up the underlying S3 location and classification, and rewrites the query to read from disk:

glue.create_database(DatabaseInput={"Name": "analytics"})
glue.create_table(
    DatabaseName="analytics",
    TableInput={
        "Name": "users",
        "StorageDescriptor": {"Location": "s3://my-bucket/tables/users/"},
        "Parameters": {"classification": "csv"},
    },
)

athena.start_query_execution(
    QueryString="SELECT name FROM analytics.users WHERE age > 25",
    QueryExecutionContext={"Database": "analytics"},
    ResultConfiguration={"OutputLocation": "s3://my-bucket/results/"},
)

No more hand-written read_csv('s3://...'). Mixed queries that combine a Glue-managed table with explicit s3:// URIs in the same statement also resolve correctly.

Second, completed queries now persist their results to the configured OutputLocation as <query_id>.csv plus a <query_id>.csv.metadata companion file (column names + Athena-mapped types). The CSV begins with the column-name header row, matching real Athena's output format — Glue crawlers and BI tools that expect header=True read it without configuration.

Pre-release, we also offloaded the DuckDB run to a worker thread via asyncio.to_thread. The merged PR scheduled it via asyncio.create_task, which would have stalled the event loop on the single-process server for the full query duration. The fix means multiple concurrent Athena queries now run on parallel threads while the loop stays free.

EventBridge rules now dispatch to Step Functions state machine targets

EventBridge rules with a target ARN of the form arn:aws:states:<region>:<account>:stateMachine:<name> previously fell through to the "unsupported target type" warning and silently dropped events — even though Step Functions has been a first-class service in MiniStack for releases. Now the dispatcher routes them through stepfunctions._start_execution:

sm_arn = sfn.create_state_machine(
    name="audit-pipeline",
    definition=json.dumps({"StartAt": "Done", "States": {"Done": {"Type": "Pass", "End": True}}}),
    roleArn="arn:aws:iam::000000000000:role/sfn-role",
)["stateMachineArn"]

eb.put_targets(
    Rule="my-rule",
    EventBusName="my-bus",
    Targets=[{"Id": "sfn-target", "Arn": sm_arn}],
)

eb.put_events(Entries=[{
    "Source": "myapp.audit",
    "DetailType": "UserLogin",
    "Detail": json.dumps({"userId": "u-1"}),
    "EventBusName": "my-bus",
}])

# Execution starts with the EventBridge envelope as input
execs = sfn.list_executions(stateMachineArn=sm_arn)["executions"]

The transformed payload (post Input / InputPath / InputTransformer) is passed verbatim as the execution input, so all the Input* features work for free. RoleArn on the target is accepted and ignored — same posture as the Lambda/SQS/SNS dispatchers, since MiniStack doesn't enforce IAM. Multi-tenancy is preserved end-to-end: the request's account contextvar flows through the dispatcher into the daemon-thread execution snapshot.

Upgrade

docker pull ministackorg/ministack:1.3.43
docker run -d -p 4566:4566 ministackorg/ministack:1.3.43

Or pin in compose.yaml:

services:
  ministack:
    image: ministackorg/ministack:1.3.43
    ports:
      - "4566:4566"

For IoT Core, use the 1.3.43-full tag (includes the cryptography package needed for the Local CA):

docker pull ministackorg/ministack:1.3.43-full

Stay in sync

Issues and PRs welcome on GitHub. Discussion on r/ministack.