site logo

Marico's space

使用 Floci 在本地运行 AWS CodePipeline、CodeBuild 和 CodeDeploy

服务器技术 2026-11-13 17:34:31 6

最近折腾 Floci,在本地搭了一套 AWS CodePipeline、CodeBuild 和 CodeDeploy 流水线,中间踩了几个坑。这篇把完整流程、模拟器的差异和绕坑方案讲清楚:不用 AWS 账号,也不会产生账单。

为了在本地运行 AWS 持续集成/持续交付(CI/CD)流水线,我把 AWS 命令行界面(CLI)指向 Floci——一个本地 AWS 模拟器,再用 PowerShell 脚本编排每个阶段。流水线会构建 Java Web 应用,把它部署到一台所谓的“EC2 实例”上,实际上就是一个 Docker 容器,最后通过 localhost 对外提供服务。

大部分流程按原计划跑通了,唯独 CodeDeploy 钩子没有执行。于是我补了一个轻量级的 SSM(Systems Manager)代理替代方案,专门处理这些钩子。

项目地址:axololtos/cicd-pipeline-floci。整体设计参考了免费的 NextWork DevOps 挑战,但这个仓库与 NextWork 官方无关。

为什么要在本地模拟

按照教程的常规做法,需要准备真实账号、配置 IAM(Identity and Access Management,身份与访问管理)、创建并运行 EC2(Elastic Compute Cloud,弹性计算云)实例,最后仔细清理资源。漏掉任何一个步骤,都可能继续产生费用。

Floci 直接消除了这类风险,整套环境也可以反复重建:07-cleanup.ps1 负责清理,按编号排列的脚本则负责重新搭建。

代价就是模拟精度。Floci 实现的是 API(Application Programming Interface,应用程序编程接口)层面的能力,并不一定覆盖底层的全部行为。本文重点就是这些差异暴露出来之后,该怎么绕过去。

整体架构

Architecture

阶段 服务 职责
Source GitHub 托管 Maven Web 应用
Packages CodeArtifact mvn deploy 发布到的 Maven 仓库
Build CodeBuild 构建 WAR 包并上传到 S3
Deploy CodeDeploy + EC2 + SSM 通过钩子脚本获取 WAR 包并启动 Tomcat
Orchestrate CodePipeline 串联 Source、Build、Deploy

技术栈包括:运行在 Docker 中的 Floci(挂载 Docker Socket,因此 EC2 实例会变成容器)、PowerShell 5.1+、AWS CLI v2、Docker Desktop,以及 Maven 3.5.2 / JDK(Java Development Kit,Java 开发工具包)8 构建镜像。脚本位于 scripts/:

01-start-floci.ps1 02-webapp.ps1 03-codeartifact.ps1 04-codebuild.ps1
05-deploy.ps1 06-pipeline.ps1 07-cleanup.ps1 08-screenshots.ps1
env.ps1 ssm-agent.ps1 appspec.yml compose.yaml hooks/

让 CLI 指向 Floci

env.ps1 集中保存了全部 AWS 配置:伪造的访问凭证和一个服务地址。

$env:AWS_ENDPOINT_URL = "http://localhost:4566"
$env:AWS_DEFAULT_REGION = "us-east-1"
$env:AWS_ACCESS_KEY_ID = "test"
$env:AWS_SECRET_ACCESS_KEY = "test"
$env:AWS_PAGER = ""

AWS_ENDPOINT_URL 是新版 CLI v2 提供的全局地址覆盖变量,因此所有命令都不需要再追加 --endpoint-url。访问密钥随便填即可。执行 aws sts get-caller-identity 会返回账号 000000000000,这也说明命令确实连到了 Floci。

同一个文件还定义了 $MavenImage = "maven:3.5.2-jdk-8" 和一个 Mvn 辅助方法,用一次性容器运行 Maven,因此宿主机不需要安装 JDK。

首次运行的输出包含调用者身份、三个 IAM 角色、两个 S3 存储桶,以及 CodeArtifact 仓库。

Floci identity, roles, buckets and CodeArtifact repo

在 Floci 控制台中看到的资源如下:

Floci console: IAM roles

Floci console: S3 buckets

构建:CodeArtifact 和 CodeBuild

03-codeartifact.ps1 会创建三个 IAM 角色:codebuild-role、codedeploy-role 和 codepipeline-role;接着创建 CodeArtifact 域 nextwork 和仓库 nextwork-devops-cicd,最后从容器里执行 mvn deploy。重复运行时会出现 ConflictException ... already exists,这不影响后续操作。

CodeArtifact domain and repo creation, ConflictException on re-run

CodeArtifact domain, repository and Maven endpoint

04-codebuild.ps1 会创建两个存储桶,使用 tar.exe 打包源代码,确保生成的压缩包能在 Linux 环境中使用,然后把压缩包上传到 s3://nextwork-source/source.zip,再创建构建项目。脚本会持续轮询,直到 buildComplete 为 true,最后列出构建产物:存储桶 nextwork-artifacts 中有一个 2,291 字节的 nextwork-web-project.war。

Buckets, source upload and CodeBuild project

Build polled to SUCCEEDED, WAR listed in S3

Maven 在 17.269 秒后输出 BUILD SUCCESS,控制台里也能看到对应的构建日志组:

Maven BUILD SUCCESS

Floci 默认不会真正执行 IAM 策略校验,所以这里只需要角色 ARN 存在即可。在真实 AWS 上,如果信任策略写错,构建会直接失败。

部署:模拟器的差距暴露出来了

05-deploy.ps1 会创建密钥对和开放 8080 端口的 web-sg 安全组,然后执行 aws ec2 run-instances。Floci 会启动一个名为 floci-aws-ec2-<instance-id> 的容器,并在容器中安装 Corretto 8、Tomcat 9.0.89 和 AWS CLI。

Key pair, security group on 8080, instance launched as a container

Floci console: nextwork-web instance running

随后,脚本会将实例注册到 SSM,把 scripts/hooks 复制到容器的 /scripts 目录,再重新创建 CodeDeploy 应用和部署组。

Dependencies installed, instance registered with SSM, hooks copied, CodeDeploy app and group created

问题一:基于标签匹配不到实例

正常情况下,部署组会通过 EC2 标签筛选目标实例。但 Floci 中这次查询没有结果,因此脚本改为把实例注册成 on-premises 实例,并且不给部署组设置任何过滤条件:

aws deploy create-deployment-group --application-name nextwork-app --deployment-group-name nextwork-dg ` --service-role-arn arn:aws:iam::000000000000:role/codedeploy-role
aws deploy register-on-premises-instance --instance-name $id ` --iam-user-arn arn:aws:iam::000000000000:user/devops-iam

问题二:没人执行钩子

在真实 AWS 上,实例内的代理会拉取命令并执行 appspec.yml 中定义的钩子。Floci 只会把命令放进 SSM 队列等待代理处理,但默认没有这个代理。如果命令在大约 500 毫秒内没有被确认,钩子就会失败。

解决办法是让 05-deploy.ps1 启动 ssm-agent.ps1 后台作业。该脚本每 100 毫秒轮询一次 GetMessages,确认收到的每条消息,对负载进行 Base64 解码,再通过 docker exec <container> sh -c 执行,最后调用 SendReply 回传状态、退出码和输出。

这个代理必须在创建部署之前启动。05 还会先清空队列中的历史命令,避免上一次失败运行留下的消息占满队列,导致正常部署错过确认窗口。

Agent log: each hook run through docker exec, exit code 0, Deployment status Succeeded

钩子做了什么

Floci 会跳过部署包下载和安装,因此钩子脚本需要自行从 S3 获取 WAR 包。最终两个钩子都以退出码 0 执行完毕,部署状态为 Succeeded,上面的代理日志可以看到完整过程。

Floci 的端口转发器把容器中的 8080 端口映射到了宿主机的 30000 端口:

The deployed page

组装流水线

06-pipeline.ps1 会接收 GitHub 用户名,把流水线定义写入 pipeline.json,然后创建流水线:Source 阶段使用 GitHub,监听 main 分支;Build 阶段引用 nextwork-build;Deploy 阶段引用 nextwork-app 和 nextwork-dg;产物存储桶则使用 nextwork-artifacts。

pipeline.json: Source stage and artifact store

aws codepipeline create-pipeline --cli-input-json "file://$f"
aws codepipeline start-pipeline-execution --name nextwork-pipeline

第一次轮询时,Build 阶段还是 InProgress;下一次轮询,三个阶段已经全部变成 Succeeded:

Pipeline execution state and known-gap notes

最后通过 AWS CLI 检查完整资源状态:

Build SUCCEEDED, instance running, deployment group, pipeline stages Succeeded

这套方案不能证明什么

  • CodeBuild 读取的是 s3://nextwork-source/source.zip,不是 GitHub 产物。 Floci 的 CodeBuild 只接受 S3 源或空源。要发布代码修改,需要编辑应用,然后重新执行 04 和 05。
  • 流水线的 Deploy 阶段会直接返回成功,不会执行钩子。 本文实际验证过的钩子链路,来自 05-deploy.ps1 发起的直接部署。
  • Floci 使用内存存储。 构建脚本本身也给出了警告:Floci 重启后,构建产物会丢失。
  • CodeArtifact 不支持上游依赖解析,因此 Maven 插件仍然从 Maven Central 获取。
  • IAM 策略不会被真正执行,所以权限配置错误无法被这套环境发现。

因此,这套环境主要用于演练流水线的结构和 API 调用,并不能证明同一份流水线定义放到真实 AWS 上也一定能通过。

运行方式

需要准备 Docker Desktop、AWS CLI v2、PowerShell 5.1+ 和 Git。第 2 步需要一个空的 GitHub 仓库地址,第 6 步需要你的 GitHub 用户名。

cd scripts
Set-ExecutionPolicy -Scope Process Bypass
.\01-start-floci.ps1
.\02-webapp.ps1 -RepoUrl https://github.com/YOU/nextwork-web-project.git
.\03-codeartifact.ps1
.\04-codebuild.ps1
.\05-deploy.ps1
.\06-pipeline.ps1 -GitHubUser YOU
.\07-cleanup.ps1

执行完 05 后,使用脚本输出的端口打开 http://localhost:<port>/。我这次运行得到的是 30000。

下一步

  • 让 CodeBuild 读取流水线中的 GitHub 源产物,不再依赖预先压缩并上传到 S3 的对象。
  • 让 Deploy 阶段真正执行钩子,并通过页面的可见变化验证部署结果。

项目地址:github.com/axololtos/cicd-pipeline-floci。如果这套方案帮你省下了一笔 AWS 账单,可以给仓库点个 Star,后续我还会继续分享更多实践内容。