Quarkus Local AWS Emulation Without the Auth Token Tax
A practical Dev Services pattern for teams that need seeded S3, SSM, and SQS in dev and CI without wiring every service by hand.
I like local AWS dependencies that start when I run quarkus dev, wire themselves into the SDK clients, and stay out of a real account. No signup wall. No auth token in CI. No hand-maintained compose file for every service. Floci is an open-source local AWS emulator, and the Quarkus Amazon Services extensions already treat it as a Dev Services stack provider. The switch is mostly configuration.
In this walkthrough we build InvoiceVault for Parchment & Co., a fictional document logistics firm. The service accepts invoice PDFs over HTTP, stores them in Amazon S3 (Simple Storage Service), reads per-customer limits from AWS Systems Manager Parameter Store (SSM), and publishes an event to Amazon SQS (Simple Queue Service). When you finish, you have a working API, seeded parameters, and tests that prove the flow without touching AWS.
What we build
InvoiceVault exposes one write endpoint:
POST /invoices/{customerId}/{invoiceId}withContent-Type: application/pdf
Behind it:
SSM supplies
/invoicevault/customers/{customerId}/max-size-mband/invoicevault/sqs-urlS3 stores objects in bucket
invoicesunder key{customerId}/{invoiceId}.pdfSQS queue
invoices-receivedgets a small JSON message per upload
Floci runs in a container through Dev Services. Quarkus points the generated S3Client, SqsClient, and SsmClient at that endpoint automatically.
Prerequisites
You need a current JDK, a container runtime (Docker or Podman), and either the Quarkus CLI or Maven. The AWS CLI is optional but useful when you want to inspect buckets and queues manually.
JDK 21
Quarkus CLI (
quarkus create app) or Maven 3.9+Docker or Podman for Dev Services
Optional: AWS CLI v2 for manual verification against http://localhost:4566
About ☕️☕️
Project setup
Create the application without the default greeting resource:
quarkus create app dev.quarkex:invoicevault \
--extension='quarkus-amazon-s3,quarkus-amazon-sqs,quarkus-amazon-ssm,quarkus-rest-jackson' \
--java=21 \
--no-codequarkus create adds the Amazon Services platform BOM (quarkus-amazon-services-bom) next to the Quarkus platform BOM. The extensions we need are quarkus-amazon-s3, quarkus-amazon-sqs, quarkus-amazon-ssm, and quarkus-rest-jackson. I verified this guide on Quarkus 3.35.4 with the matching Amazon Services BOM line.
Under src/main/java, create the package directories for dev.quarkex.invoicevault (the listings below use that package throughout).
Configure Dev Services for Floci
Open src/main/resources/application.properties:
quarkus.aws.devservices.provider=floci
quarkus.aws.devservices.floci.image-name=floci/floci:latest-compat
quarkus.aws.devservices.floci.init-scripts-folder=src/test/resources/floci-init
%dev.quarkus.aws.devservices.floci.container-properties.FLOCI_STORAGE_MODE=hybridquarkus.aws.devservices.provider=floci tells every Amazon Services Dev Services instance in this project to start Floci instead of the default LocalStack image. You can override a single service with quarkus.s3.devservices.provider=floci if you run a mixed setup.
floci/floci:latest-compat is the image that bundles Python 3, boto3, and the AWS CLI for initialization hooks. Shell or Python seed scripts need the compat image. The standard floci/floci:latest image is smaller but will fail a hook that calls aws with exit code 127.
init-scripts-folder=src/test/resources/floci-init points Dev Services at a project folder on disk. Quarkus mounts those scripts into Floci’s ready hook directory during startup. Put scripts directly in that folder (01-seed-aws.py, not nested under ready.d/ — Quarkus handles the mount target).
FLOCI_STORAGE_MODE=hybrid in %dev only keeps in-memory performance with periodic disk flush so a container restart does not wipe local state. Floci’s default is memory; set hybrid explicitly for day-to-day dev.
Dev Services also sets the endpoint, region, and credentials on the SDK clients. You do not need quarkus.s3.endpoint-override or static access keys in %dev unless you point at a remote stack.
Seed SSM and SQS before the first request
Create src/test/resources/floci-init/01-seed-aws.py:
#!/usr/bin/env python3
import boto3
from botocore.exceptions import ClientError
s3 = boto3.client("s3")
sqs = boto3.client("sqs")
ssm = boto3.client("ssm")
try:
s3.create_bucket(Bucket="invoices")
except ClientError as error:
code = error.response.get("Error", {}).get("Code", "")
if code not in ("BucketAlreadyOwnedByYou", "BucketAlreadyExists"):
raise
queue_url = sqs.create_queue(QueueName="invoices-received")["QueueUrl"]
ssm.put_parameter(
Name="/invoicevault/customers/cust-001/max-size-mb",
Value="10",
Type="String",
Overwrite=True,
)
ssm.put_parameter(
Name="/invoicevault/sqs-url",
Value=queue_url,
Type="String",
Overwrite=True,
)Mark it executable (chmod +x). Floci runs .py hooks with python3 on the compat image; boto3 is preconfigured for http://localhost:4566, so you do not need --endpoint-url flags.
The hook runs inside the Floci container during the ready phase, before your application serves traffic. It creates the S3 bucket, the SQS queue, the SSM parameters, and writes the queue URL into Parameter Store. That keeps infrastructure out of Java startup.
If you prefer shell and AWS CLI, use the same logic in a .sh file — but keep the compat image. A hook that calls aws against the standard floci/floci:latest image fails fast and shuts the container down.
Application startup
With the init hook in place, Quarkus startup is boring in the right way:
Dev Services pulls
floci/floci:latest-compatand starts the container.Floci runs
01-seed-aws.pyand finishes the ready phase.Quarkus wires
S3Client,SqsClient, andSsmClientto the mapped endpoint.InvoiceServicecomes up with no@PostConstructAWS calls — it assumes the bucket, queue, and parameters already exist.
That is the same path for ./mvnw quarkus:dev and ./mvnw test. You should not need Java code to recreate resources the init hook already created.
If the first upload fails with Missing SSM parameter, the hook did not run. Check Dev Services logs for Floci startup errors (wrong image, hook exit code, or container runtime mount issues).
InvoiceService
InvoiceService is upload logic only: read the customer limit from SSM, store the PDF in S3, publish the event to SQS.
package dev.quarkex.invoicevault;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.ParameterNotFoundException;
@ApplicationScoped
public class InvoiceService {
static final String BUCKET = "invoices";
static final String QUEUE_URL_PARAM = "/invoicevault/sqs-url";
@Inject
S3Client s3;
@Inject
SqsClient sqs;
@Inject
SsmClient ssm;
public String store(String invoiceId, byte[] pdf, String customerId) {
int maxSizeMb = readMaxSizeMb(customerId);
long maxBytes = (long) maxSizeMb * 1024 * 1024;
if (pdf.length > maxBytes) {
throw new IllegalArgumentException(
"Invoice exceeds max size of " + maxSizeMb + " MB for customer " + customerId);
}
String key = customerId + "/" + invoiceId + ".pdf";
s3.putObject(
PutObjectRequest.builder().bucket(BUCKET).key(key).contentType("application/pdf").build(),
RequestBody.fromBytes(pdf));
String queueUrl = ssm.getParameter(r -> r.name(QUEUE_URL_PARAM)).parameter().value();
String body = """
{"invoiceId":"%s","customerId":"%s","key":"%s"}
""".formatted(invoiceId, customerId, key).strip();
sqs.sendMessage(SendMessageRequest.builder().queueUrl(queueUrl).messageBody(body).build());
return key;
}
private int readMaxSizeMb(String customerId) {
String paramName = "/invoicevault/customers/" + customerId + "/max-size-mb";
try {
return Integer.parseInt(ssm.getParameter(r -> r.name(paramName)).parameter().value());
} catch (ParameterNotFoundException e) {
throw new IllegalStateException("Missing SSM parameter " + paramName, e);
}
}
}The split is deliberate. SSM is the configuration plane, S3 is the blob store, and SQS is the async hand-off. Missing parameters fail fast with IllegalStateException instead of silently accepting uploads.
REST endpoint
Expose the service over JAX-RS:
package dev.quarkex.invoicevault;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Map;
@Path("/invoices")
public class InvoiceResource {
@Inject
InvoiceService invoiceService;
@POST
@Path("/{customerId}/{invoiceId}")
@Consumes("application/pdf")
@Produces(MediaType.APPLICATION_JSON)
public Response upload(
@PathParam("customerId") String customerId,
@PathParam("invoiceId") String invoiceId,
byte[] body) {
String key = invoiceService.store(invoiceId, body, customerId);
return Response.ok(Map.of("key", key)).build();
}
}What breaks in real use
Missing SSM parameters. If the init hook did not run, the first upload dies in readMaxSizeMb. Check Dev Services logs for Floci hook failures and confirm /invoicevault/customers/cust-001/max-size-mb exists before you hit the REST endpoint.
Wrong queue URL in SSM. A stale URL makes sendMessage fail or land on a queue you are not watching. The init hook creates the queue and writes the returned URL into SSM so the two stay aligned.
Missing S3 bucket. If the hook failed after creating parameters but before create_bucket, uploads fail at putObject. Re-run dev mode or inspect Floci logs for hook exit codes.
Floci is not production AWS. It is for dev and CI. IAM simulation, edge-case service APIs, and Pro-only LocalStack features may differ. Floci documents 51 emulated services; verify your surface area before you standardize on it org-wide.
Nested Docker services. Lambda, RDS, and ElastiCache paths inside Floci spawn real containers and need the Docker socket. InvoiceVault’s S3/SQS/SSM path does not.
Prove it
Add AssertJ to pom.xml for readable assertions. The example here uses version 3.27.3. Create src/test/resources/sample-invoice.pdf (a minimal PDF is enough) and src/test/java/dev/quarkex/invoicevault/InvoiceVaultTest.java:
package dev.quarkex.invoicevault;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.Message;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest;
import software.amazon.awssdk.services.ssm.SsmClient;
@QuarkusTest
class InvoiceVaultTest {
@Inject
InvoiceService invoiceService;
@Inject
S3Client s3;
@Inject
SqsClient sqs;
@Inject
SsmClient ssm;
@Test
void shouldStoreInvoicePublishEventAndEnforceMaxSize() throws IOException {
byte[] pdf;
try (InputStream in = getClass().getResourceAsStream("/sample-invoice.pdf")) {
assertThat(in).isNotNull();
pdf = in.readAllBytes();
}
String key = invoiceService.store("INV-TEST-001", pdf, "cust-001");
assertThat(key).isEqualTo("cust-001/INV-TEST-001.pdf");
try (ResponseInputStream<GetObjectResponse> object = s3.getObject(
GetObjectRequest.builder().bucket(InvoiceService.BUCKET).key(key).build())) {
assertThat(object.readAllBytes()).isEqualTo(pdf);
}
String queueUrl = ssm.getParameter(r -> r.name(InvoiceService.QUEUE_URL_PARAM))
.parameter()
.value();
List<Message> messages = sqs.receiveMessage(ReceiveMessageRequest.builder()
.queueUrl(queueUrl)
.maxNumberOfMessages(1)
.waitTimeSeconds(2)
.build())
.messages();
assertThat(messages).hasSize(1);
assertThat(messages.get(0).body()).contains("INV-TEST-001").contains("cust-001").contains(key);
byte[] tooLarge = new byte[11 * 1024 * 1024];
assertThatThrownBy(() -> invoiceService.store("INV-TOO-LARGE", tooLarge, "cust-001"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("max size");
}
}Run:
./mvnw testDev Services starts Floci, runs init scripts when mounts succeed, and executes the test against the same clients you use in dev mode. One green test is enough here.
Manual checks in dev mode
Start the app:
./mvnw quarkus:devWatch the log for Dev Services pulling floci/floci:latest-compat and a line like Dev Services (amazon-floci) for Amazon Services started. Floci advertises fast native startup, but your machine, pull cache, and container runtime still decide what you see. You should not see bucket or SSM seeding logs from Java — that work belongs to the init hook.
Upload a sample PDF:
curl -s -X POST http://localhost:8080/invoices/cust-001/INV-2026-001 \
-H "Content-Type: application/pdf" \
--data-binary @src/test/resources/sample-invoice.pdfExpected JSON body: {"key":"cust-001/INV-2026-001.pdf"}.
With the AWS CLI pointed at Floci, use the endpoint Dev Services prints. In tests that is often a mapped host port. In dev mode, check the log for You can connect to the stack at http://localhost:PORT:
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
ENDPOINT=http://localhost:43201
aws --endpoint-url="${ENDPOINT}" s3 ls s3://invoices/cust-001/
aws --endpoint-url="${ENDPOINT}" sqs receive-message \
--queue-url "$(aws --endpoint-url="${ENDPOINT}" ssm get-parameter \
--name /invoicevault/sqs-url --query Parameter.Value --output text)" \
--max-number-of-messages 1You should see the PDF key in S3 and a JSON message on the queue.
Storage modes (when you outgrow defaults)
Floci exposes FLOCI_STORAGE_MODE on the container. Their docs describe four modes:
memory— the simplest ephemeral setup, which is a good fit for CI.hybrid— the mode I use here for local dev.persistent— a more eager persistence option.wal— the most durability-oriented option of the four.
We set hybrid only in %dev above. Check the current Floci docs for the exact flush and durability behavior before you standardize on one mode across a team. For CI, I would usually leave the test profile on the default ephemeral setup unless I had a reason to keep state longer.
Conclusion
You now have InvoiceVault: PDF ingest, SSM-backed limits, durable object keys in S3, and an SQS notification, all running on Floci through Quarkus Dev Services without touching a cloud account. The nice part is not the single property line, even though quarkus.aws.devservices.provider=floci is what flips the stack. The nice part is that the Amazon Services extensions keep the SDK clients, tests, and container lifecycle aligned, so local work stays boring in the best possible way.
Full source on my Github repository: https://github.com/myfear/the-main-thread/invoicevault


