AI模型服务化实战:Docker+AWS Fargate快速部署指南 1. 项目概述当模型跑通了却卡在“上线”这道门上你是不是也经历过这样的时刻凌晨三点Jupyter Notebook里那个LSTM模型终于把时序预测的MAE压到了0.87验证集曲线漂亮得像教科书插图你兴奋地截图发到团队群配文“搞定”然后——就再也没然后了。接下来两周你一边改PPT向业务方汇报“模型已ready”一边在公司内网Wiki里翻找“如何把Flask API部署到测试环境”一边被运维同事礼貌但坚定地告知“Python 3.9运行时还没进基线镜像等排期”。这不是个例这是绝大多数数据科学家职业生涯里最真实、最沉默的挫败感我们花了80%的时间让模型“聪明”却要用120%的精力让模型“活着”。而这篇博文要讲的就是怎么把那120%压缩到两小时以内——不是靠加班而是靠一次对齐工程逻辑的重构。核心关键词是Docker容器化、AWS Fargate无服务器计算、模型服务化Model-as-a-Service、环境一致性、配置与代码分离。它不面向专职云架构师而是专为那些手握PyTorch模型、熟悉scikit-learn API、但看到CloudFormation模板就头皮发麻的数据从业者准备的。你不需要从零学AWS IAM策略也不用啃完Kubernetes调度原理只需要理解一个朴素事实模型不是部署在服务器上而是部署在“可执行的环境快照”里而Fargate就是帮你自动管理这个快照生命周期的管家。我用自己上线的三个生产模型一个实时风控评分API、一个电商商品推荐微服务、一个IoT设备异常检测端点反复验证过这套流程从本地训练完的.pkl或.pt文件到获得一个带HTTPS的公网URL全程可复现、可审计、可回滚且月度云账单稳定在$47–$89区间。下面我们就从“为什么必须容器化”这个最常被跳过的环节开始拆解。2. 内容整体设计与思路拆解放弃“直接部署”拥抱“环境交付”2.1 为什么传统部署方式在AI场景下必然失败很多数据科学家的第一反应是“我的模型代码只有200行直接扔到EC2上跑个Flask不就行了”——这想法本身没错但错在低估了AI工作流的“隐性依赖复杂度”。让我用一个真实案例说明去年我接手一个NLP情感分析模型原作者留下的README写着“pip install -r requirements.txt”而requirements.txt里只有一行transformers4.25.1。看起来很干净对吧但实际部署时我们卡在了第7步EC2启动Ubuntu 20.04apt update apt install python3-pippip install -r requirements.txt→ 失败提示torch未安装手动pip install torch1.13.1cpu -f https://download.pytorch.org/whl/torch_stable.html再次pip install -r requirements.txt→ 成功启动Flaskpython app.py→ 报错OSError: libglib-2.0.so.0: cannot open shared object fileapt install libglib2.0-0→ 终于启动但调用时返回CUDA out of memory问题出在哪不是代码错了而是整个执行环境的“状态”无法被精确描述和复现。transformers4.25.1依赖torch1.12.0但没指定CPU/GPU版本libglib是系统级库pip根本管不着而CUDA内存错误是因为EC2实例类型选了t3.medium无GPU但模型代码里写了model.to(cuda)。这些都不是bug而是环境契约的缺失。传统部署把“代码”和“环境”当成两个独立对象管理而AI模型恰恰是二者强耦合的一个sklearn.ensemble.RandomForestClassifier在Python 3.8和3.11下可能因底层NumPy编译器差异导致预测结果漂移0.3%一个TensorFlow 2.8模型在CUDA 11.2和11.8驱动下可能触发非确定性随机数生成。所以我们的设计起点必须是交付的不是代码而是一个包含代码、依赖、系统库、甚至内核参数的完整、自包含、可验证的执行单元。这就是容器化的底层逻辑——它不是技术炫技而是对AI工程化本质的诚实回应。2.2 为什么选Docker而不是其他打包方案面对环境一致性问题有人会说“用conda环境导出environment.yml不行吗”或者“用PyInstaller打包成二进制不更轻量”——这些方案在特定场景有效但在生产AI服务中存在硬伤conda环境解决了Python包依赖但无法管理apt/yum安装的系统库如libjpeg-dev、ffmpeg也无法保证不同Linux发行版间的ABI兼容性。更重要的是conda activate myenv这种命令式操作无法被自动化流水线可靠调用。PyInstaller确实能生成单文件但对深度学习框架支持极差。PyTorch的torch._C模块是C扩展PyInstaller打包后常因动态链接路径错误崩溃TensorFlow的libtensorflow_cc.so体积超200MB打包后启动延迟高达15秒完全无法满足API毫秒级响应要求。Docker的优势在于“分层抽象”的精准匹配基础镜像层如nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu20.04固化操作系统、CUDA驱动、cuDNN版本解决GPU兼容性依赖层RUN pip install torch2.0.1cu118 -f https://download.pytorch.org/whl/torch_stable.html精确控制Python包及二进制分发渠道应用层COPY . /app RUN chmod x /app/entrypoint.sh注入模型文件、配置、启动脚本与环境解耦。关键是每一层都可缓存、可复用、可审计。我团队的CI流水线里基础镜像层更新频率是季度级依赖层是月度级而应用层是每次git push即构建。这种分层让“环境变更影响范围”变得可预测——当你只改了模型权重文件Docker只会重建最上层构建时间从12分钟降到23秒。2.3 为什么选AWS Fargate而非EC2或ECS明确了容器是载体下一个问题是谁来运行这个容器常见选项有EC2手动管理虚拟机、ECSEC2集群上调度容器、Fargate纯容器运行时。选择Fargate的核心理由是它把“基础设施即代码”的承诺兑现到了最后一厘米。EC2的问题你需要决定实例类型c5.2xlarge还是g4dn.xlarge、配置Auto Scaling组、设置安全组规则、管理AMI生命周期、处理实例故障恢复……这些工作与“让模型提供API”毫无关系却消耗了70%的部署时间。更致命的是EC2的资源粒度太粗一个g4dn.xlarge4 vCPU/16GB RAM每月$120但你的模型推理只用1.2 vCPU/3.5GB RAM剩下60%资源永远闲置。ECSEC2模式的问题虽然抽象了容器调度但你仍需维护EC2集群——监控集群CPU使用率、手动扩容节点、处理节点OS安全补丁。去年我们一个ECS集群因linux-firmware包更新导致网卡驱动失效整个推荐服务中断47分钟根源竟是运维忘了给节点打补丁。Fargate的破局点在于“按需付费的原子资源单元”你不再申请“一台服务器”而是声明“我需要2 vCPU/4GB RAM的计算能力持续运行10分钟”。Fargate后台自动为你分配物理资源任务结束立即释放。实测数据一个BERT-base文本分类API在Fargate上配置0.5 vCPU/1 GB RAM即可稳定支撑50 QPS月均成本$28.7同等负载下EC2方案最低需t3.large2 vCPU/8GB RAM月均$42.3且空闲时段资源浪费率达68%。更重要的是Fargate的task definition任务定义本身就是一份可版本化的基础设施文档——它明确记录了容器镜像URI、CPU/内存配额、环境变量、日志配置所有内容均可纳入Git仓库实现真正的“IaC”。3. 核心细节解析与实操要点从模型文件到Docker镜像的七道工序3.1 模型文件的标准化封装告别“直接加载.pth”很多数据科学家把模型保存成.pth或.pkl就以为万事大吉但生产环境要求模型文件必须是自描述、可验证、免依赖的。我强制团队执行以下三原则模型与权重分离绝不保存torch.save(model, model.pth)这种包含完整模型类定义的文件。正确做法是# 保存时只序列化权重和必要元数据 torch.save({ state_dict: model.state_dict(), # 仅权重张量 config: model.config.to_dict(), # 模型结构参数如hidden_size768 version: 1.2.0, # 模型版本号用于灰度发布 timestamp: datetime.now().isoformat() }, model_weights_v1.2.0.pth)加载时先用标准类如BertForSequenceClassification.from_pretrained(bert-base-uncased)初始化空白模型再load_state_dict()。这样即使未来PyTorch升级导致model.__dict__序列化格式变化权重文件依然可用。预处理逻辑内聚化把Tokenizer、归一化参数等全部打包进模型文件。例如# 构建一个可序列化的预处理器类 class TextPreprocessor: def __init__(self, tokenizer_namebert-base-uncased, max_len128): self.tokenizer AutoTokenizer.from_pretrained(tokenizer_name) self.max_len max_len def encode(self, texts): return self.tokenizer(texts, truncationTrue, paddingTrue, max_lengthself.max_len, return_tensorspt) # 保存预处理器 preprocessor TextPreprocessor() torch.save(preprocessor, preprocessor_v1.2.0.pkl)这样API接收原始文本内部统一调用preprocessor.encode()避免前端传参格式不一致导致的KeyError。模型文件签名与校验生产环境必须防止模型文件被篡改。我们在CI流水线中加入GPG签名# 构建完成后用团队私钥签名 gpg --detach-sign --armor model_weights_v1.2.0.pth # 生成 model_weights_v1.2.0.pth.ascDocker镜像构建时RUN gpg --verify model_weights_v1.2.0.pth.asc model_weights_v1.2.0.pth校验失败则构建中断。这招帮我们拦截过两次CI服务器被入侵导致的恶意模型替换。3.2 Dockerfile编写不是写脚本而是写契约一个生产级Dockerfile不是“怎么让代码跑起来”而是“如何向世界声明这个服务的运行契约”。以下是我们的标准模板以PyTorch模型为例每行都有其不可替代的工程意义# 第1行基础镜像——契约的基石 FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu20.04 # 选择CUDA 11.8而非12.x因为PyTorch 2.0官方wheel仅支持到11.8Ubuntu 20.04是AWS Lambda/EC2的长期支持基线 # 第2-3行系统依赖——契约的硬件层 RUN apt-get update apt-get install -y --no-install-recommends \ libglib2.0-0 \ libsm6 \ libxext6 \ rm -rf /var/lib/apt/lists/* # 这些是OpenCV、Matplotlib等库的底层依赖缺一不可--no-install-recommends减少攻击面 # 第4-5行Python环境——契约的语言层 ENV PYTHONUNBUFFERED1 ENV PYTHONDONTWRITEBYTECODE1 # 强制Python输出实时日志禁用.pyc缓存避免多实例间字节码冲突 # 第6-8行Python依赖——契约的软件层 COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip RUN pip install --no-cache-dir -r requirements.txt # --no-cache-dir确保每次构建都拉取最新包避免本地pip缓存污染--upgrade pip防旧版pip解析依赖失败 # 第9-11行应用注入——契约的交付物 WORKDIR /app COPY . . RUN chmod x /app/entrypoint.sh # entrypoint.sh是唯一启动入口隔离启动逻辑与容器生命周期 # 第12-13行运行时约束——契约的边界 EXPOSE 8000 HEALTHCHECK --interval30s --timeout3s --start-period5s --retries3 \ CMD curl -f http://localhost:8000/health || exit 1 # EXPOSE声明端口是文档行为HEALTHCHECK是Fargate健康检查的依据必须返回HTTP 200 # 第14行执行契约——契约的履行 ENTRYPOINT [/app/entrypoint.sh]关键细节绝不使用pip install在线安装requirements.txt必须锁定所有包版本如torch2.0.1cu118避免pip install torch拉取到不兼容的nightly版本。WORKDIR必须显式声明隐式/目录会导致权限问题Fargate默认以非root用户运行。HEALTHCHECK必须真实有效不能只是echo OK必须调用模型实际推理路径。我们/health端点会执行一次轻量级推理如输入test返回置信度0.9确保GPU/CUDA真正就绪。3.3 entrypoint.sh让容器“活”起来的启动引擎很多人把启动逻辑全写在CMD里但entrypoint.sh才是生产环境的真正大脑。它要解决三个核心问题环境初始化、服务就绪等待、优雅退出。我们的标准脚本如下#!/bin/bash set -e # 任何命令失败立即退出 # 1. 模型文件完整性校验生产必备 if [[ ! -f /app/model_weights_v1.2.0.pth ]]; then echo ERROR: Model weights file missing! exit 1 fi if [[ ! -f /app/preprocessor_v1.2.0.pkl ]]; then echo ERROR: Preprocessor file missing! exit 1 fi # GPG校验如果启用了签名 if [[ -f /app/model_weights_v1.2.0.pth.asc ]]; then gpg --verify /app/model_weights_v1.2.0.pth.asc /app/model_weights_v1.2.0.pth fi # 2. 环境变量安全加固 # 将敏感配置如数据库密码从环境变量转为文件挂载避免ps命令泄露 if [[ -n ${DB_PASSWORD} ]]; then echo ${DB_PASSWORD} /run/secrets/db_password unset DB_PASSWORD fi # 3. 启动Gunicorn生产WSGI服务器 # --preload 预加载模型避免每个worker重复加载 # --workers 2 根据Fargate CPU配额动态调整0.5vCPU用1 worker2vCPU用4 workers # --timeout 120 防止长尾请求拖垮服务 exec gunicorn --bind :8000 \ --workers 2 \ --worker-class uvicorn.workers.UvicornWorker \ --preload \ --timeout 120 \ --keep-alive 5 \ --log-level info \ app:app为什么不用flask run因为Flask开发服务器是单线程、无超时、无健康检查的玩具。Gunicorn是生产级WSGI服务器--preload参数让主进程加载模型一次再fork出多个worker共享内存模型加载时间从12秒降至1.8秒--timeout防止某个异常请求如超长文本阻塞整个服务。UvicornWorker则提供异步支持对IO密集型预处理如调用外部API提升30%吞吐量。4. 实操过程与核心环节实现Fargate部署的十二步落地清单4.1 前置准备AWS账户与权限最小化配置在动手前请务必完成以下三件事否则后续步骤会卡在权限拒绝创建专用IAM角色不要用AdministratorAccess创建名为FargateModelExecutionRole的角色附加以下最小权限策略{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:GetDownloadUrlForLayer, ecr:BatchGetImage ], Resource: * }, { Effect: Allow, Action: [ logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents ], Resource: arn:aws:logs:*:*:* } ] }这个策略只允许拉取ECR镜像和写CloudWatch日志杜绝了S3删除、EC2启动等高危操作。创建ECS集群在AWS控制台ECS服务中点击“创建集群”选择“Networking only (Fargate)”模板。集群名设为model-serving-prodVPC选择默认VPC或业务VPC子网至少选2个可用区如us-east-1a,us-east-1b以实现高可用。创建ECR仓库进入ECR服务创建私有仓库ml-model-api。仓库URI形如123456789012.dkr.ecr.us-east-1.amazonaws.com/ml-model-api这是后续Docker推送的目标地址。4.2 本地构建与推送让镜像“走出实验室”假设你的项目目录结构如下ml-model-api/ ├── Dockerfile ├── entrypoint.sh ├── requirements.txt ├── app.py # FastAPI应用入口 ├── model/ │ ├── model_weights_v1.2.0.pth │ └── preprocessor_v1.2.0.pkl └── tests/ └── test_api.py执行以下命令请替换YOUR_AWS_ACCOUNT_ID和REGION# 1. 登录ECR获取临时凭证 aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com # 2. 构建镜像标签为模型版本号便于追踪 docker build -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/ml-model-api:v1.2.0 . # 3. 推送镜像注意首次推送需先创建仓库 docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/ml-model-api:v1.2.0提示推送速度慢在~/.docker/config.json中添加experimental: enabled并启用buildkitexport DOCKER_BUILDKIT1。实测构建时间缩短40%尤其对大型基础镜像。4.3 创建Fargate任务定义基础设施的“宪法文件”任务定义Task Definition是Fargate的宪法它声明了服务的全部运行时契约。在ECS控制台点击“任务定义”→“创建新任务定义”选择“Fargate”启动类型任务定义名称ml-model-api-task任务内存根据模型大小选择。我们的BERT-base模型用4GB对应Fargate2 vCPU/4GB配置轻量级XGBoost用1GB0.5 vCPU/1GB。任务CPU2 vCPU对应4GB内存档位网络模式awsvpc推荐提供独立弹性网卡容器配置容器名称model-server镜像URI123456789012.dkr.ecr.us-east-1.amazonaws.com/ml-model-api:v1.2.0端口映射容器端口8000与Dockerfile中EXPOSE一致环境变量添加MODEL_VERSIONv1.2.0供应用读取、LOG_LEVELINFO日志配置选择awslogs驱动日志组设为/ecs/ml-model-api健康检查勾选“启用健康检查”路径/health间隔30秒超时3秒健康阈值2不健康阈值2存储与卷无需配置模型文件已打包进镜像高级选项任务执行角色选择之前创建的FargateModelExecutionRole任务角色留空除非需要访问S3等AWS服务启动类型FARGATE点击“创建”任务定义即生效。此时你已拥有一份可Git版本化的JSON文件可在AWS CLI中用aws ecs describe-task-definition --task-definition ml-model-api-task导出。4.4 创建Fargate服务让服务“永不掉线”任务定义只是蓝图服务Service才是让它持续运行的实体。在ECS集群页面点击“服务”→“创建服务”启动类型FARGATE任务定义选择刚创建的ml-model-api-task:v1注意选择修订版本服务名称ml-model-api-service服务数量1单实例足够Fargate自动处理故障恢复网络配置VPC选择你的业务VPC子网勾选至少2个跨可用区的子网如us-east-1a,us-east-1b安全组创建新安全组sg-ml-model-api入站规则仅开放TCP 8000来源0.0.0.0/0或业务ALB安全组负载均衡关键负载均衡器类型Application Load Balancer选择现有ALB如果你已有ALB选它否则点击“创建新的ALB”ALB配置名称alb-ml-model-api方案internet-facing公网可访问监听器HTTP:80→ 重定向到HTTPS:443HTTPS监听器选择ACM证书如*.yourdomain.com目标组创建新目标组tg-ml-model-api协议HTTP端口8000健康检查路径/health服务发现无需配置简单场景自动扩缩暂不启用先验证单实例稳定性点击“创建服务”Fargate将自动在指定子网中启动ENI弹性网卡拉取ECR镜像并启动容器将容器IP注册到ALB目标组ALB对目标组执行健康检查通过后开始转发流量注意首次部署时ALB健康检查可能失败几次因模型加载耗时耐心等待2-3分钟。若持续失败检查CloudWatch日志中的/ecs/ml-model-api日志流。4.5 获取公网URL与端到端验证服务创建成功后ALB的DNS名称就是你的公网API入口。在ALB控制台找到alb-ml-model-api复制其DNS名称如alb-ml-model-api-1234567890.us-east-1.elb.amazonaws.com。现在进行端到端验证# 1. 测试健康检查端点应返回200 curl -I https://alb-ml-model-api-1234567890.us-east-1.elb.amazonaws.com/health # 2. 测试真实推理POST JSON数据 curl -X POST https://alb-ml-model-api-1234567890.us-east-1.elb.amazonaws.com/predict \ -H Content-Type: application/json \ -d {text: This product is amazing!} # 预期返回 # {label:POSITIVE,confidence:0.982}如果返回504 Gateway Timeout检查ALB目标组健康状态是否显示unhealthy如果返回502 Bad Gateway检查容器日志中是否有Connection refused端口映射错误或ModuleNotFoundError依赖缺失。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “模型加载慢得像蜗牛”GPU初始化的隐藏成本现象容器启动后首次/predict请求耗时45秒后续请求正常200ms。日志显示Loading model...卡住。根因CUDA驱动在容器首次调用GPU时需执行JIT编译Just-In-Time Compilation将PTX代码编译为当前GPU架构的SASS指令。这个过程不可跳过但可优化。解决方案在entrypoint.sh中加入预热逻辑# 在exec gunicorn前添加GPU预热 if command -v nvidia-smi /dev/null; then echo Warming up GPU... # 运行一个轻量级CUDA操作 python -c import torch; x torch.randn(100,100).cuda(); y torch.mm(x,x); print(GPU warmup done) fi实测效果首次请求延迟从45秒降至3.2秒。原理是预热触发了JIT编译后续推理直接使用编译好的代码。5.2 “ALB一直显示unhealthy”健康检查的五个致命陷阱ALB健康检查失败是最高频问题90%源于配置错位。按优先级排查检查项正确配置常见错误排查命令端口映射容器端口8000ALB目标组端口8000ALB目标组端口填了80但容器只暴露8000aws ecs describe-tasks --cluster model-serving-prod --tasks task-id查看networkBindings健康检查路径ALB健康检查路径/health应用必须实现该端点应用只实现了/未写/healthcurl http://container-ip:8000/health容器内响应超时ALB健康检查超时≥5秒应用/health必须≤该值应用/health做了完整推理耗时8秒在/health中只做return {status:ok}不调用模型安全组ALB安全组出站允许0.0.0.0/0容器安全组入站允许ALB安全组容器安全组只允许22端口未开8000aws ec2 describe-security-groups --group-ids sg-idVPC路由ALB和容器在同VPC子网路由表指向IGW子网是private subnet无IGW路由aws ec2 describe-route-tables --route-table-ids rtb-id提示在ALB监听器中将健康检查路径临时改为/若变健康则100%是应用未实现/health端点。5.3 “日志里全是乱码”字符编码的静默杀手现象CloudWatch日志中中文显示为日志条目出现UnicodeEncodeError。根因Docker基础镜像如Ubuntu默认locale是C不支持UTF-8。解决方案在Dockerfile中显式设置# 在FROM之后RUN之前添加 ENV LANGC.UTF-8 ENV LC_ALLC.UTF-8并在entrypoint.sh开头添加# 强制Python使用UTF-8 export PYTHONIOENCODINGutf-8此问题在处理中文文本、日志记录错误堆栈时高频出现修复后日志可读性提升100%。5.4 “成本突然飙升十倍”Fargate的隐形计费陷阱现象某天云账单显示Fargate费用暴涨但服务QPS无变化。根因Fargate按分配的资源×运行时间计费而非实际使用率。如果你的任务定义配置了4 vCPU/8GB但模型只用0.5 vCPU/1GB你仍为全部资源付费。排查方法进入CloudWatch选择ECS Per-Container Metrics选择你的集群和任务查看CPUUtilization和MemoryUtilization指标若CPUUtilization长期10%说明资源配置严重过剩优化策略阶梯式降配先将CPU从4 vCPU降至2 vCPU观察CPUUtilization是否70%再降至1 vCPU直至出现CPUThrottled告警启用自动扩缩当CPUUtilization持续70%达5分钟自动增加任务数非增加单任务资源冷启动优化对低频服务如每小时调用1次的报表API改用Lambda按毫秒计费Fargate专注高并发服务我们一个日均10万次调用的推荐API通过将配置从4 vCPU/8GB优化至1 vCPU/2GB月成本从$189降至$47性能无损。5.5 “模型预测结果每天都不一样”随机种子的幽灵现象相同输入不同时间调用API返回的置信度小数点后3位有差异如0.923 vs 0.921。根因PyTorch/TensorFlow的随机数生成器在每次进程启动时若未显式设置seed会基于系统时间初始化导致非确定性。解决方案在模型加载代码中全局固定所有随机源import torch import numpy as np import random def set_seed(seed42): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 多GPU np.random.seed(seed) random.seed(seed) torch.backends.cudnn.deterministic True # 确保CUDA卷积确定性 torch.backends.cudnn.benchmark False # 关闭benchmark避免不同输入触发不同算法 # 在app.py顶部调用 set_seed(42)此设置让模型在任意环境本地/EC2/Fargate下对相同输入产生完全相同的浮点输出满足金融、医疗等合规场景要求。6. 持续交付与演进从单次部署到自动化流水线6.1 GitOps驱动的CI/CD流水线设计单次手动部署只是起点真正的效率来自自动化。我们用GitHub Actions构建了端到端流水线# .github/workflows/deploy-model.yml name: Deploy ML Model to Fargate on: push: branches: [main] paths: - Dockerfile - requirements.txt - app.py - model/** jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentialsv2 with: role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole aws-region: us-east-1