【免费下载链接】floci
Light, fluffy, and always free - The AWS Local Emulator alternative
Floci 是一款轻量、免费的开源 AWS 本地仿真器(AWS Local Emulator)。本文以 Floci 内置的 ECS 仿真能力为主线,系统讲解其 JSON 1.1 协议接入方式、集群/任务定义/任务/服务等全量操作面、Docker 与 Mock 两种运行模式、FireLens 日志路由的底层实现,以及 host 卷、EFS 卷等安全与存储配置,并结合仓库源码给出可验证的实现依据。读完本文,你将能够基于 Floci 在本机完整走通「创建集群 → 注册任务定义 → 运行任务 → 创建服务 → 消费事件」的 ECS 开发/测试闭环,并理解其与真实 AWS 的行为差异。
协议与接入方式
ECS 服务在 Floci 中遵循 AWS 的 JSON 1.1 协议:
- Protocol:JSON 1.1
- Endpoint:
POST /+ 请求头X-Amz-Target: AmazonEC2ContainerServiceV20141113.<Action>
也就是说,任何 AWS SDK 或 CLI 只要把 endpoint 指向 Floci(默认端口4566),并把目标操作名替换成AmazonEC2ContainerServiceV20141113前缀下的对应 Action,就能像调用真实 ECS 一样工作。请求分发与参数解析由 EcsJsonHandler.java 完成,它根据X-Amz-Target头解析出 Action 并路由到 EcsService.java 中的对应方法。
从源码结构看,ECS 仿真覆盖了完整的资源模型:集群(EcsCluster)、任务定义(TaskDefinition)、任务(EcsTask)、服务(EcsServiceModel)、任务集(TaskSet)、容器实例(ContainerInstance)、容量提供方(CapacityProvider)、服务部署与修订(ServiceDeployment/ServiceRevision),以及标签、账号设置、Agent 回调等附属能力,模型类集中在 ecs/model 目录下。
核心运行模式有两种:默认配置下任务以真实 Docker 容器方式运行;设置mock: true(测试中自动启用)后任务以进程内桩方式运行,不依赖 Docker。
支持的操作面
集群(Clusters)
| 操作 | 说明 |
|---|---|
CreateCluster | 创建集群(幂等) |
DescribeClusters | 描述一个或多个集群 |
ListClusters | 列出集群 ARN |
UpdateCluster | 更新集群设置 |
UpdateClusterSettings | 更新containerInsights等设置 |
PutClusterCapacityProviders | 为集群关联容量提供方 |
DeleteCluster | 删除空集群 |
对应的服务端实现见 EcsService.java 中的createCluster、updateCluster、putClusterCapacityProviders、deleteCluster等方法,集群状态字段(status、registeredContainerInstancesCount、runningTasksCount、activeServicesCount等)由 EcsCluster.java 承载。
任务定义(Task Definitions)
| 操作 | 说明 |
|---|---|
RegisterTaskDefinition | 注册任务定义的新修订版 |
DescribeTaskDefinition | 按family:revision或 ARN 描述任务定义 |
ListTaskDefinitions | 列出任务定义 ARN |
ListTaskDefinitionFamilies | 列出任务定义 family 名称 |
DeregisterTaskDefinition | 将某修订版标记为INACTIVE |
DeleteTaskDefinitions | 删除一个或多个任务定义 |
两个值得注意的兼容性设计:
runtimePlatform与容器的logConfiguration会原样存储并按原样返回,因此像 Terraform 这类「读回自己写入内容做漂移校验」的客户端不会看到差异。runtimePlatform不影响本地任务的运行位置——Floci 始终以宿主机自身架构启动任务。firelensConfiguration同样原样存储与返回,RegisterTaskDefinition会拒绝缺失或不受支持的type(仅接受fluentd与fluentbit),详见下文 FireLens 专节。
volumesFrom条目也被存储并返回;在 Docker 模式下,源容器会先于消费方启动,其声明的卷按请求的只读/读写模式被继承,启动顺序同时尊重 FireLens 路由器的依赖关系,涉及卷继承与日志路由的循环依赖会在容器启动前被拒绝。相关实现位于 EcsContainerManager.java 的orderForDependencies与launchOrder逻辑,并有 EcsJsonHandlerVolumesFromTest.java 等测试覆盖。
任务(Tasks)
| 操作 | 说明 |
|---|---|
RunTask | 启动一个或多个任务实例 |
StartTask | 在指定容器实例上启动任务 |
StopTask | 停止运行中的任务 |
DescribeTasks | 描述一个或多个任务 |
ListTasks | 列出任务 ARN(可按集群、family、服务、状态过滤) |
UpdateTaskProtection | 设置任务的缩容保护 |
GetTaskProtection | 获取当前任务保护状态 |
任务的生命周期模型只占据PENDING、RUNNING、STOPPED三个状态,但通过事件发布器在 EventBridge 上合成完整的 AWS 阶段阶梯(见下文事件专节)。
服务(Services)
| 操作 | 说明 |
|---|---|
CreateService | 创建长期运行的服务 |
UpdateService | 更新期望实例数、任务定义或部署配置 |
DeleteService | 删除服务(支持force) |
DescribeServices | 描述一个或多个服务(含deployments,见下) |
ListServices | 列出集群中的服务 ARN |
ListServicesByNamespace | 按 Cloud Map 命名空间过滤服务 |
CreateService时 Floci 会记录并回显deploymentController、schedulingStrategy、availabilityZoneRebalancing(AWS 在创建时默认ECS/REPLICA/ENABLED),模型见 EcsServiceModel.java。
服务部署(Service deployments)
处于ACTIVE状态的服务在services[].deployments中恰好报告一条PRIMARY记录,它是根据服务当前状态合成的,而不是跟踪一次真实滚动发布。这正是 AWS 的ServicesStablewaiter 所接受的形式,因此aws ecs wait services-stable、SDK 内置 waiter 以及 Terraform 的aws_ecs_service都能正常收敛。已删除(INACTIVE)的服务返回空列表。
rolloutState在runningCount达到desiredCount时为COMPLETED,之前为IN_PROGRESS。- 部署
id由服务 ARN 与任务定义派生,因此跨调用、跨重启稳定,任务定义变更时滚动更新。 createdAt跟踪的是部署而非服务:它是服务的创建时间,直到任务定义变更开启新部署,此后随新部署移动。
已知与 AWS 的差异:
- 永远不会出现与
PRIMARY并存的第二个ACTIVE排水部署。运行中的任务确实会滚动到新任务定义(先启动新修订版的替换任务,间隔一个 reconciler tick 再排空旧任务),但deployments列表始终只报告单条PRIMARY。 - 每个服务都会报告
deployments。AWS 对使用CODE_DEPLOY或EXTERNAL部署控制器的服务会省略该字段,Floci 无论控制器类型如何都会合成deployments列表。 DAEMON调度策略为每个ACTIVE容器实例恰好运行一个任务,并据此推导desiredCount;与 AWS 一致,Fargate 启动类型以及CODE_DEPLOY/EXTERNAL控制器会拒绝该策略。放置约束不会被评估。pendingCount恒为0,与顶层服务字段一致。forceNewDeployment(任务定义未变化时)会铸造新的部署id并滚动运行中的任务:新部署的替换任务先启动,一个 reconciler tick 后排空旧部署任务;deployments列表全程仍只报告单条PRIMARY。updatedAt等于createdAt。AWS 会在滚动推进中更新它,而 Floci 没有中间滚动状态可报告。
上述行为在 EcsDescribeServicesIntegrationTest.java 中有直接断言(如deployments[0].status == "PRIMARY"、rolloutState == "COMPLETED"及其 reason),并在 EcsServiceRolloutTest.java、EcsServiceDaemonSchedulingTest.java 中验证滚动与 DAEMON 调度。
ECS EventBridge 事件
Floci 将 AWS 形状的生命周期事件发布到默认EventBridge 总线(source: aws.ecs)。匹配aws.ecs的规则会从 ECS 活动中触发,Docker 与 mock 两种模式均生效。发布逻辑见 EcsEventPublisher.java。
detail-type | 触发时机 | 关键detail字段 |
|---|---|---|
ECS Task State Change | 任务启动或停止 | lastStatus、desiredStatus、taskDefinitionArn、group、startedBy、stoppedReason、containers[].exitCode |
ECS Deployment State Change | 服务部署启动、进行中或达到稳态 | eventType(恒为INFO)、eventName、deploymentId |
eventName取值为SERVICE_DEPLOYMENT_STARTED、SERVICE_DEPLOYMENT_IN_PROGRESS、SERVICE_DEPLOYMENT_COMPLETED。
已知与 AWS 的差异:
- 任务阶段阶梯是合成的。Floci 的任务模型只占据
PENDING、RUNNING、STOPPED,但启动时依次发出PROVISIONING -> PENDING -> ACTIVATING -> RUNNING,停止时依次发出DEACTIVATING -> STOPPING -> DEPROVISIONING -> STOPPED,每个阶段一条ECS Task State Change,因此按detail.lastStatus过滤的规则行为与 AWS 一致。源码中START_LADDER/STOP_LADDER常量在 EcsEventPublisher.java,且合成过程基于任务快照而非共享实例,避免并发DescribeTasks观察到任务从未真正占据的中间状态。 SERVICE_DEPLOYMENT_FAILED与部署熔断器(circuit breaker)不会被发出。SubmitTaskStateChange/SubmitContainerStateChange保持纯 ACK;Floci 自己驱动任务生命周期,而非依赖 agent 提交。
相关测试见 EcsEventBridgeIntegrationTest.java 与 EcsEventPublisherTest.java。
未知服务(Unknown services)
解析不到的服务引用会出现在failures中,reason: MISSING,并携带该服务本应拥有的 ARN,而不是从响应中丢弃。因此DescribeServices返回部分结果而非报错(与 AWS 一致),aws ecs wait services-stable作用于不存在的服务时会立即失败,而不是轮询到超时。以 ARN 形式提供的引用会被原样回显。
任务集(Task Sets)
| 操作 | 说明 |
|---|---|
CreateTaskSet | 在服务内创建任务集 |
UpdateTaskSet | 更新任务集的规模 |
DeleteTaskSet | 删除任务集 |
DescribeTaskSets | 描述服务的任务集 |
UpdateServicePrimaryTaskSet | 将任务集提升为主任务集 |
容器实例(Container Instances)
| 操作 | 说明 |
|---|---|
RegisterContainerInstance | 向集群注册容器实例 |
DeregisterContainerInstance | 注销容器实例 |
DescribeContainerInstances | 描述容器实例 |
ListContainerInstances | 列出容器实例 ARN |
UpdateContainerAgent | 触发 agent 更新(桩) |
UpdateContainerInstancesState | 排空或激活容器实例 |
容量提供方(Capacity Providers)
| 操作 | 说明 |
|---|---|
CreateCapacityProvider | 创建自定义容量提供方 |
UpdateCapacityProvider | 更新容量提供方 |
DeleteCapacityProvider | 删除容量提供方 |
DescribeCapacityProviders | 描述容量提供方(含 FARGATE 内置项) |
服务部署与修订(Service Deployments & Revisions)
| 操作 | 说明 |
|---|---|
DescribeServiceDeployments | 描述服务部署 |
ListServiceDeployments | 列出服务部署 ARN |
DescribeServiceRevisions | 描述服务修订 |
标签(Tags)
| 操作 | 说明 |
|---|---|
TagResource | 为集群、服务、任务或任务定义添加标签 |
UntagResource | 移除资源标签 |
ListTagsForResource | 列出资源标签 |
账号设置与属性(Account Settings & Attributes)
| 操作 | 说明 |
|---|---|
PutAccountSetting | 为调用用户设置账号级设置 |
PutAccountSettingDefault | 设置默认账号级设置 |
DeleteAccountSetting | 删除账号设置 |
ListAccountSettings | 列出账号设置 |
PutAttributes | 在资源上设置自定义键值属性 |
DeleteAttributes | 移除资源属性 |
ListAttributes | 列出带指定属性的资源 |
Agent / 状态变更桩(Agent / State Change Stubs)
| 操作 | 说明 |
|---|---|
SubmitTaskStateChange | Agent 回调桩 |
SubmitContainerStateChange | Agent 回调桩 |
SubmitAttachmentStateChanges | Agent 回调桩 |
DiscoverPollEndpoint | 返回 agent 轮询端点 |
配置
Docker 后端的awsvpc任务会获得一个仿真的 ENI,并且在FLOCI_NETWORK_SECURITY_GROUP_ENFORCEMENT_ENABLED=true时,任务内所有容器共享一个受保护的网络命名空间。没有显式安全组的任务使用其子网 VPC 的默认安全组;同一任务内的容器可以通过 localhost 互相通信。bridge 与 host 网络模式不附加任务级awsvpc安全组。Mock 模式只报告控制面状态,不执行包过滤。安全组强制相关实现见 EcsContainerManager.java 的prepareNetwork与SecurityGroupFirewallManager,并有 EcsContainerManagerSecurityGroupTest.java 覆盖。
| 变量 | 默认值 | 说明 |
|---|---|---|
FLOCI_SERVICES_ECS_ENABLED | true | 启用或禁用 ECS 服务 |
FLOCI_SERVICES_ECS_MOCK | false | 跳过 Docker;任务直接进入RUNNING(适合 CI) |
FLOCI_SERVICES_ECS_DOCKER_NETWORK | (未设置) | 任务容器使用的 Docker 网络 |
FLOCI_SERVICES_ECS_DEFAULT_MEMORY_MB | 512 | 任务定义未提供内存时的默认内存(MB) |
FLOCI_SERVICES_ECS_DEFAULT_CPU_UNITS | 256 | 任务定义未提供 CPU 时的默认 CPU 单位 |
FLOCI_SERVICES_ECS_HOST_VOLUME_ROOTS | (未设置) | host 卷绑定挂载(volumes[].host.sourcePath)的受批准父目录 |
FLOCI_SERVICES_ECS_ALLOW_UNSAFE_HOST_VOLUMES | false | 允许任意 host 路径,绕过HOST_VOLUME_ROOTS白名单;路径穿越、裸根目录与 Docker socket 仍始终被拒绝 |
上述默认值在 EmulatorConfig.java 的EcsServiceConfig接口中有明确声明(enabled默认true、mock默认false、defaultMemoryMb默认512、defaultCpuUnits默认256、allowUnsafeHostVolumes默认false)。
host 卷安全(Host volume safety)
任务定义中的volumes[].host.sourcePath是调用方控制的文件系统路径,Floci 会将其直接绑定挂载进启动的容器,因此RegisterTaskDefinition与RunTask时实际绑定挂载这两个时机都会校验它(第二次检查缩小了校验与挂载之间的窗口,同时覆盖了本策略出现之前注册的任务定义)。核心实现见 HostVolumePolicy.java,它被EcsJsonHandler(注册时校验一次)与EcsContainerManager(每次绑定挂载前再次校验)共享。
无论何种配置,以下路径始终被拒绝:
- 相对路径,以及任何包含
..段的路径。 - 裸文件系统根目录(
/)。 - Docker daemon socket 及任何包含它的目录(如
/var/run、/run、/var),包括通过符号链接解析到这些路径之一的情况。受保护的 socket 是 Floci 自身 Docker 客户端连接的那个,解析方式与客户端一致:floci.docker.docker-host→DOCKER_HOST→ 当前激活的 Docker context(Colima、OrbStack、Rancher Desktop、Podman)→/var/run/docker.sock。常规位置/var/run/docker.sock与/run/docker.sock始终受保护。
从源码看,socket 候选集合还包含 Docker Desktop 的按用户 rootless socket(~/.docker/run/docker.sock),并且只有unix://端点才命名文件系统路径(tcp://与 Windows 命名管道没有可暴露的文件路径),见 HostVolumePolicy.java。
默认情况下,不做任何配置时每个 hostsourcePath都会被拒绝。必须显式选择以下方式之一:
FLOCI_SERVICES_ECS_HOST_VOLUME_ROOTS:逗号分隔的受批准父目录白名单。sourcePath必须(含符号链接解析后)位于其中一个目录之下:services: floci: image: floci/floci:latest environment: FLOCI_SERVICES_ECS_HOST_VOLUME_ROOTS: /srv/floci/volumes,/dataFLOCI_SERVICES_ECS_ALLOW_UNSAFE_HOST_VOLUMES=true:允许任意 host 路径(适用于本地开发时任何 host 路径都应可挂载的场景)。上述穿越、裸根与 Docker socket 拦截不会被该开关绕过。
被拒绝的sourcePath会使RegisterTaskDefinition以InvalidParameterException失败;若在RunTask时才被再次捕获(例如策略出现前注册的任务定义),任务会以该消息作为stoppedReason停止。命名的 Docker 卷、EFS 卷以及没有sourcePath的 host 卷(临时、容器本地存储)不受这些检查影响。相关实现证据也可在HostVolumePolicy.validate()的异常路径(相对路径、..段、裸根、socket、白名单外)中逐一对应。
EFS 卷所有权(EFS volume ownership)
任务的efsVolumeConfiguration卷由共享的本地 Docker 卷支撑(Floci 无法挂载真实的 EFS 文件系统)。Docker 命名卷以root:root 0755创建,因此以非 rootUSER运行镜像的任务无法写入。要模拟 EFS access point 的RootDirectory.CreationInfo与PosixUser,可配置floci.storage.efs.*(全部为可选启用;默认是普通命名卷,现有行为不变):
Key(floci.storage.efs.) | Env | AWS 等价物 | 说明 |
|---|---|---|---|
owner-uid | FLOCI_STORAGE_EFS_OWNER_UID | CreationInfo.OwnerUid | 卷根的所有者 uid(须与owner-gid同时设置) |
owner-gid | FLOCI_STORAGE_EFS_OWNER_GID | CreationInfo.OwnerGid | 卷根的所有者 gid(须与owner-uid同时设置) |
root-permissions | FLOCI_STORAGE_EFS_ROOT_PERMISSIONS | CreationInfo.Permissions | 3-4 位八进制数字,如0777,或用2775表达 setgid 位 |
mount-user | FLOCI_STORAGE_EFS_MOUNT_USER | PosixUser {Uid,Gid} | 以uid[:gid]运行挂载容器 |
mount-group-add | FLOCI_STORAGE_EFS_MOUNT_GROUP_ADD | PosixUser补充组 | 追加到挂载容器的补充 gid |
init-image | FLOCI_STORAGE_EFS_INIT_IMAGE | — | 用于对卷根执行一次性chown/chmod的镜像(默认busybox:stable) |
owner-uid与owner-gid必须同时设置(AWS 上部分的CreationInfo也是非法的)。卷根按卷只初始化一次;setgid 位应通过 4 位root-permissions(如2775)表达,以便子目录继承所有者 gid。EFS 卷挂载的 Docker 集成测试见 EcsContainerManagerEfsIsolationDockerIntegrationTest.java。
Mock 模式
设置FLOCI_SERVICES_ECS_MOCK=true可在无 Docker 环境下运行。此模式下任务跳过容器启动,立即进入RUNNING,停止时再进入STOPPED。这是单元/集成测试与 Docker-in-Docker 不可用的 CI 流水线的推荐模式。
# docker-compose.yml — CI / test environment services: floci: image: floci/floci:latest environment: FLOCI_SERVICES_ECS_MOCK: "true"# docker-compose.yml — local development (real containers) services: floci: image: floci/floci:latest volumes: - /var/run/docker.sock:/var/run/docker.sock environment: FLOCI_SERVICES_ECS_MOCK: "false" FLOCI_SERVICES_ECS_DOCKER_NETWORK: my_networkDocker socket 要求
mock: false(默认)时,ECS 会启动真实 Docker 容器并需要 Docker socket。挂载它并设置网络,使容器之间可以互相访问。私有 registry 认证及其他 Docker 设置参见 Docker Configuration。
services: floci: image: floci/floci:latest volumes: - /var/run/docker.sock:/var/run/docker.sock environment: FLOCI_SERVICES_ECS_DOCKER_NETWORK: aws-local_defaultFireLens 日志路由的源码级剖析
FireLens 是本文档中技术密度最高的部分,其生成逻辑集中在 FirelensConfigGenerator.java,启动编排在 EcsContainerManager.java 的startTask中完成,相关测试包括 EcsJsonHandlerFirelensTest.java、EcsContainerManagerFirelensTest.java、EcsContainerManagerFirelensDockerIntegrationTest.java 与 FirelensConfigGeneratorTest.java。
注册期校验
firelensConfiguration原样存储与返回;RegisterTaskDefinition拒绝缺失或不受支持的type(仅fluentd与fluentbit)。- 使用
awsfirelens的任务必须恰好命名一个路由器:带两个 FireLens 路由器、或路由器发布了24224端口(FirelensConfigGenerator.FORWARD_PORT)的任务定义会在启动时被拒绝。
启动期动作
对于fluentbit或fluentdFireLens 容器,Floci 会在启动时:
- 生成路由器配置:unix socket 输入、bridge/awsvpc 上的 TCP forward、ECS 元数据、可选的
config-file-type=file或s3额外配置 include,以及每个awsfirelens容器一个 output。 - 先启动路由器。
- 把
logDriver: awsfirelens的应用容器指向生成的 unix socket。
配置写入路径固定:Fluent Bit 写入/fluent-bit/etc/fluent-bit.conf;Fluentd 写入/fluentd/etc/fluent.conf,且 output 插件使用@type(而非Name)。
从源码看,生成器常量包括 socket 路径/var/run/fluent.sock(SOCKET_PATH)、forward 端口24224(FORWARD_PORT)与健康检查端口8877(HEALTHCHECK_PORT),TCP forward 的监听地址统一为0.0.0.0——AWS 在 awsvpc 模式下绑定127.0.0.1,因为所有容器共享任务网络命名空间;而 Floci 仅在启用安全组强制时为 awsvpc 任务共享网络命名空间,其余场景下注入的FLUENT_HOST(路由器容器 IP)必须可达,因此路由器必须监听所有接口(tcpListen方法及注释,见 FirelensConfigGenerator.java)。路由器内存缓冲上限为路由器内存的一半,下限 25MB(memBufLimit)。生成器形态参考了 amazon-ecs-agent 的firelensconfig_unix.go。
端点注入与「不覆盖」原则
只有从endpoint属性读取 URL 的 output 插件(s3、cloudwatch、firehose)会收到Endpoint,其值被设为 Floci 的容器可达基础 URL。原因:Fluent Bit AWS 插件只从自身配置读取自定义端点,而忽略注入容器中的AWS_ENDPOINT_URL环境变量,若不注入,路由器会把日志发往真实服务。任务定义 log options 中显式设置的endpoint永不被覆盖,因此把某个 output 指向真实 AWS 仍然可行;而在@INCLUDE引入或config-file-type=s3的配置中声明的 output 对 Floci 不可见,会保留其原有的任何 endpoint。
注意:上游 C 插件(cloudwatch_logs、kinesis_firehose、kinesis_streams)被原样保留。它们把endpoint当作裸主机名交给getaddrinfo(而非解析为 URL),并且总是发起 TLS 拨号,因此 Floci 的http://host:port基础 URL 在那里会以Misformatted domain name失败,裸主机名又无法通过证书校验,且没有 output 级开关可以禁用其中任意一项。在aws-for-fluent-bit3.x 系列上这些插件还支持独立的port(对kinesis_firehose与cloudwatch_logs未文档化但可用),因此 output 可以指向 Floci 的端口,但仍无法完成 TLS 握手。这些 output 会去往任务定义指向的任何地方。
注入的http://端点还会附带tls Off。Fluent Bit 1.9(aws-for-fluent-bit2.x 与:latest系列)在 HTTP S3 上仍会调用flb_tls_session_create,并在 NULL TLS context 上 SIGSEGV;仅凭 scheme 不够。任务定义已设置的tls会被保留(disableTlsForHttpEndpoint的实现见 FirelensConfigGenerator.java)。Fluentd output 不注入endpoint(其 AWS 插件是 Ruby gem 而非 Fluent Bit Go/C 插件)。
config-file-type=s3 的边界
Floci 遵循 ECS 自身的边界:
RegisterTaskDefinition对 Fargate 兼容的任务定义拒绝config-file-type=s3,报Fargate launch type does not support FirelensConfiguration config file from 's3';对非 S3 对象 ARN 的config-file-value报Invalid arn syntax。- Fargate 任务仍可按 AWS 文档的方式从 S3 取配置:给 aws-for-fluent-bit init 进程提供
aws_fluent_bit_init_s3_*环境变量。ECS 从不检查这些变量,Floci 直接透传,因此这里的注册也被接受;Floci 本地也不拉取任何内容——它不提供 ECS 任务元数据端点,而 init 进程在下载前会先读取该端点。 - 在 EC2 兼容的任务定义上,Floci 从自身 S3 读取对象,写入生成配置旁的固定
external.conf路径(/fluent-bit/etc/external.conf或/fluentd/etc/external.conf)并从那里 include,与 ECS agent 使用的路径一致。 - 对象在任何容器创建之前读取,因此 bucket/key 缺失会使任务以 agent 的原因停止——
Unable to download firelens s3 config file: unable to download s3 config <key> from bucket <bucket>: <detail>——而不会泄漏已启动的路由器。
相关测试见 EcsFirelensS3ConfigTest.java。
Floci 同样不校验任务定义的compatibilities/requiresCompatibilities与RunTask的launchType是否匹配:Fargate 兼容定义仍可用launchType=EC2运行(反之亦然),与缺失元数据端点时注册被接受的方式一致。共享网络命名空间(AppConfig agent 在127.0.0.1:2772)尚未实现。
其他日志驱动(包括awslogs)仍通过 Floci 流向 CloudWatch,而不是使用配置的驱动。
实战示例
AWS CLI 完整流程
export AWS_ENDPOINT_URL=http://localhost:4566 export AWS_DEFAULT_REGION=us-east-1 export AWS_ACCESS_KEY_ID=test export AWS_SECRET_ACCESS_KEY=test # Create a cluster aws ecs create-cluster --cluster-name my-cluster \ --endpoint-url $AWS_ENDPOINT_URL # Register a task definition aws ecs register-task-definition \ --family my-task \ --container-definitions '[ { "name": "app", "image": "nginx:latest", "cpu": 256, "memory": 512, "essential": true, "portMappings": [{"containerPort": 80, "protocol": "tcp"}] } ]' \ --requires-compatibilities FARGATE \ --cpu 256 --memory 512 \ --network-mode awsvpc \ --endpoint-url $AWS_ENDPOINT_URL # Run a task aws ecs run-task \ --cluster my-cluster \ --task-definition my-task \ --launch-type FARGATE \ --endpoint-url $AWS_ENDPOINT_URL # Create a service aws ecs create-service \ --cluster my-cluster \ --service-name my-service \ --task-definition my-task \ --desired-count 1 \ --launch-type FARGATE \ --endpoint-url $AWS_ENDPOINT_URL # List running tasks aws ecs list-tasks --cluster my-cluster \ --endpoint-url $AWS_ENDPOINT_URL # Stop a task aws ecs stop-task \ --cluster my-cluster \ --task <task-arn> \ --endpoint-url $AWS_ENDPOINT_URL # Delete a service aws ecs delete-service \ --cluster my-cluster \ --service my-service \ --force \ --endpoint-url $AWS_ENDPOINT_URLJava SDK 示例
EcsClient ecs = EcsClient.builder() .endpointOverride(URI.create("http://localhost:4566")) .region(Region.US_EAST_1) .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create("test", "test"))) .build(); // Create cluster ecs.createCluster(r -> r.clusterName("my-cluster")); // Register task definition ecs.registerTaskDefinition(r -> r .family("my-task") .containerDefinitions(c -> c .name("app") .image("nginx:latest") .cpu(256) .memory(512) .essential(true)) .requiresCompatibilities(Compatibility.FARGATE) .cpu("256") .memory("512") .networkMode(NetworkMode.AWSVPC)); // Run a task RunTaskResponse response = ecs.runTask(r -> r .cluster("my-cluster") .taskDefinition("my-task") .launchType(LaunchType.FARGATE) .count(1)); String taskArn = response.tasks().get(0).taskArn();从源码结构看,RunTask的containerOverrides按容器名匹配:override 的command替换任务定义中的命令,environment则与任务定义环境合并(见 EcsContainerManager.java 附近的处理,并有 EcsContainerManagerOverridesTest.java 覆盖)。端口映射方面,bridge/host 模式下显式hostPort会按字面发布到 Docker 宿主机(与 AWS bridge 模式一致);awsvpc 模式下每个 AWS 任务都有独立 ENI,字面 hostPort 在单一本地 Docker 宿主机上会跨任务冲突,因此 awsvpc 映射在原生模式总是使用动态 host 端口,Docker 模式下则仅暴露端口、由消费者经 Docker 网络 IP 访问(见 EcsContainerManager.java)。容器日志默认以/ecs/<family>作为日志组、<name>/<taskId>作为日志流流向 CloudWatch(见startTask中logStreamer.attach的调用)。
测试与验证
仓库在 src/test/java/io/github/hectorvent/floci/services/ecs 下提供了大量 ECS 相关测试,可作为行为契约继续深入研究:
- JSON 处理与持久化:
EcsJsonHandler*系列覆盖 FireLens、健康检查、runtimePlatform/logConfiguration原样回读、任务定义持久化、卷与volumesFrom、command/entryPoint 覆盖等。 - 容器生命周期:
EcsContainerManager*系列覆盖 Docker 集成下的端口映射、卷、EFS 卷隔离与命名、FireLens、安全组、Secrets、ECR 镜像重写、生命周期与 teardown。 - 服务行为:
EcsServiceRolloutTest、EcsServiceDaemonSchedulingTest、EcsDescribeServicesIntegrationTest、EcsServicePersistenceTest、EcsReconcilerMultiAccountServiceTest等覆盖滚动、DAEMON 调度、PRIMARY部署合成、持久化与多账号 reconciler。 - 事件:
EcsEventBridgeIntegrationTest、EcsEventPublisherTest验证任务阶梯与部署事件。 - Secrets:
EcsServiceContainerSecretsTest、SecretsManagerSelectorTest覆盖容器 secret 选择逻辑。
此外,兼容性测试套件(compatibility-tests)中的 Terraform、OpenTofu、CDK 用例也可用于端到端验证 ECS 与 IaC 工具的协同行为。
与真实 AWS 的关键差异速查
- 任务生命周期只有
PENDING/RUNNING/STOPPED,中间阶段由事件发布器在 EventBridge 上合成。 deployments始终单条PRIMARY,无并行的ACTIVE排水部署;pendingCount恒为 0;updatedAt等于createdAt。SERVICE_DEPLOYMENT_FAILED与部署熔断器不发出;SubmitTaskStateChange/SubmitContainerStateChange为纯 ACK。- 不校验
compatibilities与launchType的匹配;CODE_DEPLOY/EXTERNAL控制器下仍合成deployments。 runtimePlatform不影响任务实际运行架构(始终宿主机架构)。- 默认拒绝所有 host 卷挂载,需显式配置白名单或启用不安全开关。
- EFS 卷由共享本地 Docker 卷模拟,EFS access point 的创建信息与 POSIX 用户通过
floci.storage.efs.*可选配置模拟。
【免费下载链接】floci
Light, fluffy, and always free - The AWS Local Emulator alternative
相关推荐
floci Secrets Manager 本地仿真服务:从配置到轮换复制的完整实战指南
floci Secrets Manager 本地仿真服务:从配置到轮换复制的完整实战指南 floci 是开源的 AWS 本地仿真器(Local Emulator
Floci ECR 服务模拟:基于 registry:2 的真实 OCI 镜像仓库本地仿真
Floci ECR 服务模拟:基于 registry:2 的真实 OCI 镜像仓库本地仿真 本指南深入讲解 Floci 开源项目中对 AWS ECR(Elast
Floci 仿真 AWS BCM Data Exports 服务:CUR 2.0 / FOCUS 1.2 导出管理面完整指南
Floci 仿真 AWS BCM Data Exports 服务:CUR 2.0 / FOCUS 1.2 导出管理面完整指南 导读 本文深入讲解 Floci(A
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考