cloudemu

SDK-Compatible Server

Point real aws-sdk-go-v2, azure-sdk-for-go, and Google Cloud SDKs at cloudemu without changing your application code

SDK-Compatible Server

cloudemu ships HTTP servers that speak the real cloud SDK wire protocols across all three providers — AWS, Azure, and GCP. Point the actual aws-sdk-go-v2, azure-sdk-for-go, or cloud.google.com/go clients at it (via custom endpoint) and your production code runs unchanged against the in-memory backend.

Nothing to mock. No Docker. No accounts. The same SDK calls you'd run against real cloud APIs hit a local httptest.NewServer and get back SDK-decodable responses.

Why

cloudemu's Portable API is great for new code you write for testing. But most real apps already use the official cloud SDKs directly. Rewriting those call sites just to test against an emulator is friction. The SDK-compat server removes that friction — change the endpoint, done.

Quick start (AWS)

package main

import (
    "context"
    "net/http/httptest"

    "github.com/aws/aws-sdk-go-v2/aws"
    awsconfig "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/service/s3"

    "github.com/stackshy/cloudemu"
    awsserver "github.com/stackshy/cloudemu/server/aws"
)

func main() {
    ctx := context.Background()

    cloud := cloudemu.NewAWS()
    srv := awsserver.New(awsserver.Drivers{
        S3:         cloud.S3,
        DynamoDB:   cloud.DynamoDB,
        EC2:        cloud.EC2,
        VPC:        cloud.VPC,
        Lambda:     cloud.Lambda,
        SQS:        cloud.SQS,
        CloudWatch: cloud.CloudWatch,
    })

    ts := httptest.NewServer(srv)
    defer ts.Close()

    cfg, _ := awsconfig.LoadDefaultConfig(ctx,
        awsconfig.WithRegion("us-east-1"),
        awsconfig.WithCredentialsProvider(
            credentials.NewStaticCredentialsProvider("test", "test", ""),
        ),
    )

    // Use the REAL aws-sdk-go-v2 client — only the endpoint changes.
    client := s3.NewFromConfig(cfg, func(o *s3.Options) {
        o.BaseEndpoint = aws.String(ts.URL)
        o.UsePathStyle = true
    })

    client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String("my-bucket")})
    // ... full SDK API works
}

Region and credentials can be any dummy values — the server doesn't validate signatures.

Quick start (Azure)

import (
    "github.com/Azure/azure-sdk-for-go/sdk/azcore"
    "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
    "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
    "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"

    "github.com/stackshy/cloudemu"
    azureserver "github.com/stackshy/cloudemu/server/azure"
)

cp := cloudemu.NewAzure()
srv := azureserver.New(azureserver.Drivers{
    VirtualMachines: cp.VirtualMachines,
    BlobStorage:     cp.BlobStorage,
    CosmosDB:        cp.CosmosDB,
    Network:         cp.VNet,
    Monitor:         cp.Monitor,
    Functions:       cp.Functions,
    ServiceBus:      cp.ServiceBus,
})

// Azure SDK refuses bearer tokens over plain HTTP — use TLS.
ts := httptest.NewTLSServer(srv)

opts := &arm.ClientOptions{
    ClientOptions: azcore.ClientOptions{
        Cloud: cloud.Configuration{
            Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
                cloud.ResourceManager: {
                    Endpoint: ts.URL,
                    Audience: "https://management.azure.com",
                },
            },
        },
        Transport: ts.Client(),
    },
}

client, _ := armcompute.NewVirtualMachinesClient("sub-1", fakeCred{}, opts)

Quick start (GCP)

import (
    gcpcompute "cloud.google.com/go/compute/apiv1"
    "github.com/stackshy/cloudemu"
    gcpserver "github.com/stackshy/cloudemu/server/gcp"
    "google.golang.org/api/option"
)

cp := cloudemu.NewGCP()
srv := gcpserver.New(gcpserver.Drivers{
    Compute:        cp.GCE,
    Storage:        cp.GCS,
    Firestore:      cp.Firestore,
    Networking:     cp.VPC,
    Monitoring:     cp.CloudMonitoring,
    CloudFunctions: cp.CloudFunctions,
    PubSub:         cp.PubSub,
})

ts := httptest.NewServer(srv)

opts := []option.ClientOption{
    option.WithEndpoint(ts.URL),
    option.WithoutAuthentication(),
    option.WithHTTPClient(ts.Client()),
}

client, _ := gcpcompute.NewInstancesRESTClient(ctx, opts...)

Currently supported

AWS handlers

ServiceOperations
S3CreateBucket, DeleteBucket, ListBuckets, PutObject, GetObject, HeadObject, DeleteObject, ListObjectsV2 (prefix, delimiter, common prefixes, continuation token), CopyObject
DynamoDBCreateTable, DeleteTable, DescribeTable, ListTables, PutItem, GetItem, DeleteItem, UpdateItem (SET/REMOVE), Query, Scan (with FilterExpression), BatchWriteItem, BatchGetItem, TransactWriteItems
EC2RunInstances, DescribeInstances (filters: instance-id, instance-type, instance-state-name, tag:*), Start/Stop/Reboot/TerminateInstances, ModifyInstanceAttribute
EC2 — VPC + NetworkingVPCs, Subnets, Security Groups + ingress/egress rules, Internet Gateways, Route Tables + Routes, NAT Gateways, VPC Peering, Flow Logs, Network ACLs
EC2 — EBS + Key PairsVolumes (Create/Delete/Describe/Attach/Detach), Key Pairs
EC2 — Snapshots + AMIs + Spot + Launch TemplatesSnapshots, Images, Spot instance requests, Launch Templates
Auto ScalingCreateAutoScalingGroup, Update/Delete/Describe, SetDesiredCapacity, scaling policies
Lambda (REST + JSON)CreateFunction, GetFunction, ListFunctions, DeleteFunction, Invoke (sync)
SQS (JSON-RPC AwsJson1_0)CreateQueue, GetQueueUrl, ListQueues, DeleteQueue, SendMessage, ReceiveMessage, DeleteMessage
CloudWatch (Smithy rpc-v2-cbor)PutMetricData, GetMetricStatistics, ListMetrics, PutMetricAlarm, DescribeAlarms, DeleteAlarms

Azure handlers

All speak ARM JSON over HTTPS unless noted.

ServiceARM provider / operations
Virtual MachinesMicrosoft.Compute/virtualMachines — CreateOrUpdate, Get, List, Delete, start, powerOff, restart
Disks / Snapshots / Images / SSH Public KeysMicrosoft.Compute/{disks,snapshots,images,sshPublicKeys} — full CRUD
Blob Storage (data plane)Containers + Blobs: Create/Delete/List, PutBlob, GetBlob, DeleteBlob, CopyBlob
Cosmos DB (data plane)Databases, Containers, Documents — full CRUD with x-ms-documentdb-* headers
Virtual NetworkMicrosoft.Network/virtualNetworks — CRUD + subnets
Azure Monitormicrosoft.insights/metricAlerts and metric data ingest/read
FunctionsMicrosoft.Web/sites (Function Apps): CreateOrUpdate, Get, List, Delete + non-ARM /api/{name} invoke
Service BusMicrosoft.ServiceBus/namespaces[/queues] ARM CRUD + raw-HTTP REST data plane (POST /{ns}/{queue}/messages, DELETE /messages/head)

GCP handlers

All speak REST + JSON.

ServiceOperations
Compute EngineInstances + Disks + Snapshots + Images: insert/get/list/delete with LRO envelopes
NetworksVPCs, Subnetworks, Firewalls, Routes
Cloud Storage (GCS)Buckets + Objects: create/get/list/delete, upload, download, copy
FirestoreDocuments + Collections via :commit, :batchGet, :runQuery
Cloud MonitoringTime-series ingest/read, alert policies
Cloud Functions v1Create (LRO), Get, List, Delete (LRO), :call (sync invoke)
Pub/SubTopics + Subscriptions lifecycle, :publish, :pull, :acknowledge

Any operation not in these tables returns 501 Not Implemented or the provider's native error code (UnknownOperation, NotImplemented, NOT_FOUND).

How it works

The server is a tiny core plus a plugin-per-service model. Each service is a self-contained package under server/.

server/
├── server.go                    # core: Handler interface + dispatcher (~80 LOC)
├── wire/
│   ├── wire.go                  # shared XML/JSON helpers
│   ├── awsquery/                # AWS query-protocol decoder + XML envelope
│   ├── azurearm/                # ARM URL parser + JSON helpers + error envelope
│   └── gcprest/                 # GCP REST URL parser + Operation LRO helpers
├── aws/
│   ├── aws.go                   # awsserver.New(Drivers{...})
│   ├── s3/  ec2/  dynamodb/  lambda/  sqs/  cloudwatch/
├── azure/
│   ├── azure.go                 # azureserver.New(Drivers{...})
│   ├── virtualmachines/  disks/  snapshots/  images/  sshpublickeys/
│   ├── blob/  cosmos/  network/  monitor/  functions/  servicebus/
└── gcp/
    ├── gcp.go                   # gcpserver.New(Drivers{...})
    └── compute/  networks/  gcs/  firestore/  monitoring/
        cloudfunctions/  pubsub/

Each handler implements a two-method interface:

type Handler interface {
    Matches(r *http.Request) bool                    // detect by header/path/form
    ServeHTTP(w http.ResponseWriter, r *http.Request)
}

server.Server iterates registered handlers and dispatches to the first that claims the request. Adding a new service is one new package + one Register call. The core never changes.

Protocol detection

Each handler uses a different signal so dispatch is unambiguous within a provider:

HandlerHow it's detected
AWS DynamoDBX-Amz-Target: DynamoDB_20120810.* header
AWS SQSX-Amz-Target: AmazonSQS.* header
AWS LambdaURL prefix /2015-03-31/functions
AWS EC2Action=… in URL query or Content-Type: application/x-www-form-urlencoded POST
AWS CloudWatchSmithy-Protocol: rpc-v2-cbor header
AWS S3Fallback (everything else REST-shaped)
Azure (all ARM)URL begins with /subscriptions/{sub} and matches Microsoft.<Provider>/<Type>
Azure CosmosURL begins with /dbs/ (data plane, non-ARM)
Azure Functions invokeURL begins with /api/ (non-ARM data plane)
Azure Service Bus data planeNon-ARM URL ending in /messages or /messages/head
Azure BlobFallback (everything else non-ARM REST-shaped)
GCP Compute / NetworksURL prefix /compute/v1/
GCP Cloud Functions/v1/projects/.../locations/.../functions[/...]
GCP Pub/Sub/v1/projects/.../topics[/...] or /v1/projects/.../subscriptions[/...]
GCP Firestore/v1/projects/.../databases/.../documents[/...]
GCP Cloud Monitoring/v3/projects/.../
GCP GCSFallback (/storage/v1/ and /{bucket}/{object} direct-media)

Registration order matters when handlers share a path prefix — the provider factories register more-specific handlers ahead of catch-alls (S3, Blob, GCS) so first-match-wins resolves correctly.

Coverage status

ProviderDomains shipped
AWSStorage, Compute (full VPC stack), Database, Serverless, Message Queue, Monitoring
AzureStorage, Compute (+ Disks/Snapshots/Images/SSHKeys), Database, Serverless, Message Queue (ARM + REST data plane), Networking, Monitoring
GCPStorage, Compute (+ Disks/Snapshots/Images), Database, Serverless, Message Queue, Networking, Monitoring

The remaining 9 service domains (IAM, DNS, Load Balancer, Cache, Secrets, Logging, Notifications, Container Registry, Event Bus) have full driver implementations in providers/{aws,azure,gcp}/; SDK-compat handlers are added in lockstep across all 3 providers as each domain ships.

Writing your own handler

If you need a service we don't cover yet, implement the server.Handler interface in your own package and register it:

type MyHandler struct{ /* driver */ }

func (*MyHandler) Matches(r *http.Request) bool {
    // your detection logic
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // your logic
}

srv := server.New()
srv.Register(&MyHandler{...})

The Handler interface is the only contract — no registration is needed in core cloudemu. If the handler is generally useful, a PR to add it under server/<provider>/<service> is welcome.

Limitations

  • No signature validation. cloudemu is a local development tool, not a security boundary. Requests are accepted regardless of AWS SigV4 / Azure AAD / GCP OAuth signatures.
  • No AMQP for Azure Service Bus. The modern azservicebus SDK uses AMQP exclusively for data plane. ARM control plane is fully supported via armservicebus; tests that need send/receive can use the raw-HTTP REST data plane.
  • GCS direct-media downloads assume path-style URLs.
  • DynamoDB / Cosmos / Firestore filters and queries support common patterns but are not full DSL parsers.
  • Pagination tokens are honored where present in the SDK contract; some list operations short-circuit to a single page.

When a client hits an unsupported operation, the server responds with the provider's native error code so failures are easy to diagnose.

On this page