floci
居安思危,思则有备,有备无患。——《左传》
一台本地的 AWS:快 24 毫秒,只吃 13 MiB,然后决定哪些东西必须是真的
先说本地模拟这件事的尴尬
在本地跑云服务,是每个后端都干过的事。
尴尬的地方在于:模拟器太好用,你会忘了自己在模拟;模拟器太粗糙,你会在一周后发现「本地能跑,线上不行」。
这个矛盾没法用「覆盖更多服务」解决。因为问题不在数量,在深度——一个 S3 的 mock 可以接受 PutObject 并返回 200,但它不会告诉你版本控制、对象锁、预签名 URL 在真实环境里是怎么互相作用的。
所以当我看到 Floci 的介绍时,最先注意到的不是它支持多少服务,而是它把「哪里用真 Docker」单独列了一张表。
项目地址:https://github.com/floci-io/floci
它的定位很干脆:一个免费、开源的本地 AWS 模拟器,用于开发、测试和 CI。
口号四句:任何云,本地跑。轻、蓬松、永远免费。不要账号,不要 auth token,不要功能门槛。只要 docker compose up。
家族:四个云,四个端口
有个细节挺可爱:它是 Floci 模拟器家族里的 AWS 成员,名字取自 floccus——一种形似爆米花的卷云。
于是一张表把整个家族摆开了:
| 模拟器 | 云 | 端口 |
|---|---|---|
| floci | AWS | 4566 |
| floci-az | Azure | 4577 |
| floci-gcp | GCP | 4588 |
| floci-oci | OCI | 4599 |
四个云,四个连号端口,间隔 11。这种排布方式有一种很朴素的美感——你不用记,你只需要记住第一个。
而对本文来说,只有 4566 那一行是主角。
三个数字,先把话说清楚
它给了一张和 LocalStack Community 的对比表。我不打算复述整张,只挑三个最能说明性格的:
启动时间:约 24 毫秒,对上约 3.3 秒。
空闲内存:约 13 MiB,对上约 143 MiB。
镜像大小:约 90 MB,对上约 1.0 GB。
24 毫秒这个数字,快到不像一个「服务」。它意味着启动成本已经低到不需要考虑——你可以在每个测试类里起一个新的,跑完就扔。
而那 13 MiB 的空闲内存,解释了这个数字是怎么来的:它不是一个常驻的庞然大物,它大部分时候什么都不做。
对一个要在 CI 里跑几百次的组件来说,这两项比任何功能列表都重要。CI 里最贵的从来不是 CPU,是等待和内存配额。
顺带说,还有一个数字:原生二进制约 40 MB。
时机:一扇门关上,另一扇打开
这份文档没有回避它为什么存在。
LocalStack 社区版在 2026 年 3 月落幕,要求 auth token,并冻结了安全更新。
Floci 把自己定位成「无附加条件的替代方案」。
这个定位能成立,靠的不是情怀,是那张表里另外几行:MIT 许可、安全更新继续、API Gateway v2 和 Cognito 有、RDS / ElastiCache / MSK / Neptune / DocumentDB / ECS / EC2 / EKS / CodeBuild 用真实 Docker 跑。
一个开源项目在竞品的转折点上出现,是常态。但能不能接住,取决于它在那个时间点之前攒了多少东西——从兼容性测试的数量看,它攒了一阵子。
那条分界线:什么时候必须是真的
这是全文我最想展开的一节。
文档里有一句判断:当进程内模拟会降低保真度时,Floci 使用真实 Docker 容器,适用于有状态数据库、连接密集型协议、运行时和构建系统。
这句话把「真假」的标准说清楚了。不是「重要的用真的」,而是「会漏的用真的」。
于是一条分界线出现了。
进程内就够了:SSM、SQS、SNS、S3、DynamoDB、IAM、STS、KMS、Secrets Manager、Step Functions、CloudFormation、EventBridge、CloudWatch、API Gateway……
这些服务的本质是「一套协议 + 一份状态」。它们的正确性体现在 API 的语义上,而不是底层跑着什么。
必须是真的:Lambda 要真实的运行时环境和热容器池;RDS 要真的 PostgreSQL、MySQL、MariaDB;ElastiCache 要真的 Valkey;Neptune 要真的 Gremlin Server 或 Neo4j;MSK 要真的 Kafka 兼容 broker;EKS 要真的 k3s;MWAA 要真的 Airflow;CodeBuild 要真的执行 buildspec。
因为这些东西的正确性藏在行为里,不在协议里。你没法 mock 一个数据库的事务隔离级别,也没法 mock 一个 Kubernetes API server 的调度。
这条线画得对不对,几乎决定了一个模拟器能不能被信任。
而它连细节都给到了默认镜像:postgres:16-alpine、mysql:8.0、mariadb:11、valkey/valkey:8、redpandadata/redpanda:latest、rabbitmq:3-management、rancher/k3s:latest、opensearchproject/opensearch:2。
默认值都写出来,意味着你可以换掉它们,也意味着出问题时你知道该怀疑谁。
覆盖:一张长到需要折叠的表
服务列表按类别排,从核心应用到计费,一共十类。
粗看一眼就够震撼:S3、SQS、SNS、DynamoDB、Lambda、IAM、STS、KMS、Secrets Manager、SSM、EventBridge 全家桶、Step Functions、SWF、CloudWatch 全家桶、API Gateway 两代、AppSync、Cognito、Route53、ECS、EC2、EKS、MWAA、ECR、EFS、CodeBuild、CodeDeploy、CodePipeline、Batch、Auto Scaling、ELB 两代、Athena、Glue、EMR、Redshift、Firehose、Flink、OpenSearch、Textract、Transcribe、Comprehend、Rekognition、Translate、Bedrock、SageMaker、RDS、Neptune、DocumentDB、DMS、MemoryDB、ElastiCache、Timestream、SES、Kinesis、MSK、Amazon MQ、Transfer、IoT Core、WAF、GuardDuty、Inspector、CloudTrail、CloudFront、Organizations、Macie、Security Hub、Budgets、Pricing、Cost Explorer、CUR……
但真正见功力的是那张更细的「服务说明」表,因为它标了每个服务做到什么程度。
举几个例子。
AWS FIS:26 个管理 API 全有,但实验执行是「安全地在控制面模拟」,不会真的向其他服务注入故障。这句话写得很老实——它没说自己能真的搞挂你的东西。
Amazon Data Lifecycle Manager:策略和标签能管,但计划任务是存着不执行,不会真的产生快照或 AMI。
Application Auto Scaling:策略能存,但是惰性的。
Textract / Transcribe:明确写着 stub,返回占位数据,异步任务立即完成,不做真实音频处理。
Bedrock Runtime:返回固定的 Converse 和 InvokeModel 响应,供本地开发用。
一个模拟器最难得的品质,不是「支持」,是知道自己支持到哪一层,并且写出来。上面这几条,每一条都在防止你在本地得出一个错误的结论。
存储:四档,按场景挑
FLOCI_STORAGE_MODE 有四档:
memory:全在内存,容器停了就没了。给 CI 和临时测试。
persistent:启动时加载,每次写操作立刻落盘。要「重启还在」,但不想太复杂。
hybrid:内存性能,每 5 秒异步刷一次。本地开发的默认甜点。
wal:预写日志,每次变更先记日志再响应。最高持久性。
四档的命名很直白,没有「标准模式」「增强模式」这种糊弄人的说法。而每一档都标了「最适合」和「持久性等级」,等于把取舍摆在桌面上让你自己签字。
多账户:12 位数字就是账户 ID
这一条很妙,值得单独说。
如果你把 AWS_ACCESS_KEY_ID 设成正好 12 位数字,Floci 就把它当作账户 ID。两个账户创建的资源互相看不见。
1 | AWS_ACCESS_KEY_ID=111111111111 aws sqs create-queue --queue-name orders |
两条命令,两个同名队列,互不干扰。
其他格式的 key——比如 test 或者 AKIA...——会回落到 FLOCI_DEFAULT_ACCOUNT_ID,默认是十二个零。
STS 的临时凭证也会被路由:AssumeRole 拿到的凭证解析到被假设角色的账户,所以「跨账户 assume-role 然后建资源」这种模式在本地也能跑。
用一个早已存在的字段形状做多租户隔离,不引入新概念,不改你的 SDK 配置。这种设计属于「想到之后觉得理所当然,想不到就永远想不到」那一类。
兼容性:2576 个测试,不是形容词
很多模拟器的文档会写「高度兼容」。这一份写的是数字。
compatibility-tests 目录跨 SDK 和工具链验证,表格是这样的:
| 模块 | 语言 / 工具 | 测试数 |
|---|---|---|
| sdk-test-java | Java 17 | 1,326 |
| sdk-test-node | Node.js | 449 |
| sdk-test-python | Python 3 | 311 |
| sdk-test-go | Go | 157 |
| sdk-test-awscli | Bash | 205 |
| compat-terraform | Terraform | 67 |
| compat-opentofu | OpenTofu | 41 |
| compat-cdk | AWS CDK | 20 |
加一下:1326 + 449 + 311 + 157 + 205 + 67 + 41 + 20 = 2,576。
和它自己写的总数对得上。
这个数字的意义不在于大,在于它覆盖的方向:五个语言的 SDK,加上 CLI、Terraform、OpenTofu、CDK。也就是说,它验证的不是「API 能不能调通」,而是「不同工具链看到的世界是不是同一个」。
一个只在 boto3 上验证过的模拟器,很可能在 Terraform 的 provider 那里露馅,因为后者对返回结构的宽容度完全不同。
迁移:换一行镜像
如果这套东西的说服力还不够,那这一节大概是最后的推力。
它是 LocalStack Community 的直接替代:端口一样、凭证一样、SDK 配置一样、CLI 端点模式一样。换掉镜像就能继续。
1 | image: localstack/localstack |
环境变量会自动映射:PERSISTENCE=1 变成 FLOCI_STORAGE_MODE=persistent,DEBUG=1 变成 QUARKUS_LOG_LEVEL=DEBUG,LOCALSTACK_HOST 变成 FLOCI_HOSTNAME。
更细的还有三条:挂在 /etc/localstack/init/ 的初始化脚本不用改就能跑;/_localstack/init 和 /_localstack/health 两个端点照样提供;启动日志结尾仍然打一行 Ready.——所以那些监听这行日志的工具,比如 Testcontainers 的等待策略,行为不变。
最后这一条是最见心思的。它没有去改别人的工具,而是让自己的日志长得像别人期待的样子。
想退出这套自动翻译,设 LOCALSTACK_PARITY=false。
发布节奏:每月 15 日和最后一天
稳定版在每月 15 日和当月最后一天发布,美国中部时间 00:30 自动切版。
两次发布之间,nightly 跟着 main 走,每个合并的修复第二天就能拿到,带日期的 nightly-mmddyyyy 可以钉住某一晚的构建。
版本号由 semantic-release 从 Conventional Commits 推导,CHANGELOG.md 自动生成、从不手工编辑。发布只从 main 切出,没有维护分支。
这套节奏有一个很明显的好处:你永远知道下一次稳定版什么时候来。不用猜,不用等公告。
赞助:只买到感谢
赞助那一节写得比大多数项目都硬。
它说:Floci 是独立开源项目,靠使用它的人和公司资助。赞助只换来感谢,别无其他——每个模拟服务对所有人永久免费,任何赞助者都不会获得其他成员没有的功能、优先级或路线图影响力。
然后才列 Gold、Silver、Community 三档,权益明明白白就是 logo 位置和发布说明里的提及。
先划清「钱买不到什么」,再谈「钱能买到什么」。这个顺序,比权益本身更说明态度。
最后
回到开头那个尴尬。
本地模拟器最难的地方,从来不是「能不能跑起来」,而是「跑起来之后你敢不敢信」。
Floci 的答案分三层:用 24 毫秒和 13 MiB 让启动成本低到可以随便起;用一条清晰的分界线决定什么必须真跑;用 2576 个跨工具链的测试,证明不同客户端看到的是同一个世界。
剩下的那些做不到的地方,它也没有藏——FIS 不会真注入故障,DLM 不会真做快照,Textract 只是 stub。
一个工具愿意告诉你它在哪里是假的,比它宣称自己全真,可信得多。
至于那句「只要 docker compose up」——它确实做到了。只不过在这四个字之前,有人把「哪部分该真、哪部分可以假」想了很久。
