基于AWS Lambda的机器学习动态定价系统 CI/CD管道部署方案介绍
本篇文章Integrating CI/CD Pipelines to Machine Learning Applications详细介绍了如何将CI/CD管道集成到机器学习应用中,适合希望提升自动化和效率的开发者。文章的技术亮点在于使用AWS Lambda架构构建基础设施CI/CD管道,强调了自动化测试、构建和部署的流程,确保代码质量和安全性。
之前的一篇关联文章是:
搭建机器学习模型的数据管道架构方案
基于AWS Lambda的机器学习动态定价系统 CI/CD管道部署方案介绍
文章目录
- 1 什么是CI/CD管道
- 2 工作流程实战
- 3 测试与构建工作流程
- 3.1 添加PyTest脚本
- 3.2 配置Synk凭证用于SAST和SCA测试
- 3.3 为AWS凭证设置OIDC
- 3.4 为Github Actions配置IAM角色
- 3.5 配置AWS CodeBuild
- 3.5.1 为CodeBuild添加IAM角色
- 3.5.2 创建CodeBuild项目
- 3.5.3 添加`buildspec`文件
- 4 部署工作流程
- 5 使用Grafana进行监控
- 5.1 为Grafana创建AWS IAM用户
- 5.2 附加角色和策略
- 5.3 将数据源连接到Grafana
- 6 结论
1 什么是CI/CD管道
CI/CD管道是一套自动化流程,有助于机器学习团队更可靠、高效地交付模型。
这种自动化对于确保新模型版本在没有人工干预的情况下持续集成、测试和部署到生产环境至关重要。
在本文中,我将探讨一个分步指南,介绍如何为部署在无服务器Lambda架构上的机器学习应用集成基础设施CI/CD管道。
CI/CD(持续集成/持续交付)管道是一个自动化流程,通过自动化构建、测试和部署软件的步骤,帮助更可靠、高效地交付代码更改。
**持续集成(CI)**侧重于开发人员定期将代码更改合并到中央仓库的实践。
每次合并后,都会运行自动化构建和一系列测试(如单元测试),以确保新代码不会破坏现有应用。
**持续交付(CD)**自动化了将通过CI的代码准备好发布的过程。
在此过程中,软件被构建、测试并打包成可发布的状态。
然后,**持续部署(CD)**将通过所有自动化测试的代码自动部署到生产环境,无需人工干预。
使用CI/CD管道在DevOps实践中至关重要,它提供了以下好处:
- 更快的发布,因为自动化消除了耗时的手动操作任务,
- 降低风险,通过对每次代码更改运行自动化测试,
- 改善协作,通过共享的自动化管道提供一致的流程,以及
- 提高代码质量,通过自动化测试的即时反馈。
2 工作流程实战
为ML应用建立健壮的CI/CD管道,自动化基础设施、模型和数据的整个生命周期至关重要。
这个过程被称为MLOps,它将传统的DevOps实践扩展到包括机器学习特有的挑战,如数据和模型版本控制。
在本文中,我将重点介绍为基于AWS Lambda构建的动态定价系统构建基础设施CI/CD管道:
_图. 基础设施CI/CD管道
该管道涵盖四个阶段:
- 源(Source):向GitHub提交代码更改以触发管道,
- 测试(Test):对工件运行自动化测试和安全扫描,
- 构建(Build):将提交的代码编译成工件,以及
- 部署(Deploy):将通过测试的代码部署到预发布或生产环境。
所有代码都托管在GitHub上,并通过分支保护规则和强制拉取请求审查进行保护。
一旦更改准备就绪,GitHub Actions工作流程(图中绿色框)就会被触发,以运行测试和构建过程。
为了防止错误到达生产环境,我在构建和部署工作流程之间添加了人工审查阶段(图中粉色框),确保在最终部署之前解决任何问题。
如果代码通过人工审查,另一个GitHub Actions工作流程将手动触发,将代码作为Lambda函数部署到预发布或生产环境。
整个过程通过全面的监控和安全检查(橙色框)得到增强。
3 测试与构建工作流程
我将首先配置Github Actions工作流程,使其在每次推送和拉取请求时触发测试和构建。
这个自动化过程涉及三个阶段:
环境设置:
- 设置Python,
- 安装依赖项,
- 使用**Open ID Connect (OIDC)**配置AWS凭证,
测试阶段:
- 运行PyTest,
- 运行静态应用安全测试 (SAST),
- 使用**软件成分分析 (SCA)**扫描依赖项,
构建阶段:
- 一旦代码通过所有测试,触发AWS CodeBuild启动项目,其中容器镜像被构建并推送到ECR。
这些阶段在存储在项目根目录下的.github
文件夹中的build_test.yml
脚本中配置:
.github/workflows/build_test.yml
name: Build and Teston: push: branches: [ main ] pull_request: branches: [ main ]env: API_ENDPOINT: ${{ secrets.API_ENDPOINT }} CLIENT_A: ${{ secrets.CLIENT_A }}permissions: id-token: write contents: read security-events: writejobs: build_and_test: runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: checkout repository code uses: actions/checkout@v4- name: set up python uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip'- name: install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements_dev.txt - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ secrets.AWS_REGION_NAME }} role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }} role-session-name: GitHubActions-Build-Test-${{ github.run_id }}- name: test aws access run: | aws sts get-caller-identity echo "✅ oidc authentication successful" - name: run pytest run: pytest env: CORS_ORIGINS: 'http://localhost:3000,http://127.0.0.1:3000' PYTEST_RUN: true- name: run snyk sast uses: snyk/actions/python@master with: command: test args: --severity-threshold=high --policy-path=.synk --python-version=3.12 --skip-unresolved --file=requirements.txt env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: run snyk sca uses: snyk/actions/python@master with: command: code test args: --severity-threshold=high --policy-path=.synk --python-version=3.12 --skip-unresolved --file=requirements.txt env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}- name: trigger aws codebuild uses: aws-actions/aws-codebuild-run-build@v1 id: codebuild with: project-name: ${{ secrets.CODEBUILD_PROJECT }} source-version-override: ${{ github.sha }} env-vars-for-codebuild: | GITHUB_SHA=${{ github.sha }}, BUILD_TYPE=test - name: check codebuild status if: always() run: | BUILD_ID="${{ steps.codebuild.outputs.aws-build-id }}" echo "codebuild id: $BUILD_ID" BUILD_STATUS=$(aws codebuild batch-get-builds --ids "$BUILD_ID" \ --query 'builds[0].buildStatus' --output text) echo "build status: $BUILD_STATUS" aws codebuild batch-get-builds --ids "$BUILD_ID" \ --query 'builds[0].phases[].{Phase:phaseType,Status:phaseStatus,Duration:durationInSeconds}' \ --output table if [ "$BUILD_STATUS" != "SUCCEEDED" ]; then echo "❌ codebuild failed with status: $BUILD_STATUS" exit 1 else echo "✅ codebuild completed successfully" fi - name: upload build artifacts if: always() run: | echo "build completed for commit: ${{ github.sha }}" echo "branch: ${{ github.ref_name }}" echo "build ID: ${{ steps.codebuild.outputs.aws-build-id }}"
接下来,我将添加支持组件以使工作流程成功运行。
此过程包括:
- 添加PyTest脚本,
- 为SAST和SCA测试配置Synk凭证,
- AWS相关配置:
- 为AWS凭证设置OIDC,
- 为GitHub Actions定义IAM角色,以及
- 配置AWS CodeBuild
让我们来看看。
3.1 添加PyTest脚本
我将通过将PyTest脚本添加到项目仓库根目录下的tests
文件夹来启动此过程。
为了演示,我将添加两个测试文件来评估main
脚本和Flask的app
脚本:
tests/main_test.py
(测试main脚本)
import os
import shutil
import numpy as np
import pytestimport src.main as main_scriptdef test_data_loading_and_preprocessor_saving(mock_data_handling, mock_s3_upload, mock_joblib_dump): """tests that data loading is called and the preprocessor is saved and uploaded.""" main_script.run_main()mock_data_handling.assert_called_once()mock_joblib_dump.assert_called_once_with(mock_data_handling.return_value[-1], PREPROCESSOR_PATH)mock_s3_upload.assert_any_call(file_path=PREPROCESSOR_PATH)def test_model_optimization_and_saving(mock_data_handling, mock_model_scripts, mock_s3_upload): """tests that each model's optimization script is called and the results are saved and uploaded.""" mock_torch_script, mock_sklearn_script = mock_model_scripts main_script.run_main() assert mock_torch_script.called assert mock_sklearn_script.call_count == len(main_script.sklearn_models)assert os.path.exists(DFN_FILE_PATH) mock_s3_upload.assert_any_call(file_path=DFN_FILE_PATH)assert os.path.exists(SVR_FILE_PATH) mock_s3_upload.assert_any_call(file_path=SVR_FILE_PATH)assert os.path.exists(EN_FILE_PATH) mock_s3_upload.assert_any_call(file_path=EN_FILE_PATH)assert os.path.exists(GBM_FILE_PATH) mock_s3_upload.assert_any_call(file_path=GBM_FILE_PATH)
tests/app_test.py
(测试Flask应用脚本)
import os
import json
import io
import pandas as pd
import numpy as np
from unittest.mock import patch, MagicMockimport appos.environ['CORS_ORIGINS'] = 'http://localhost:3000, http://127.0.0.1:3000'@patch('app.t.scripts.load_model')
@patch('torch.load')
@patch('app._redis_client', new_callable=MagicMock)
@patch('app.joblib.load')
@patch('app.s3_load_to_temp_file')
@patch('app.s3_load')
def test_predict_endpoint_primary_model( mock_s3_load, mock_s3_load_to_temp_file, mock_joblib_load, mock_redis_client, mock_torch_load, mock_load_model, flask_client,): """test a prediction from the primary model without cache hit.""" mock_preprocessor = MagicMock() mock_joblib_load.return_value = mock_preprocessor mock_s3_load.return_value = io.BytesIO(b'dummy_data') mock_s3_load_to_temp_file.return_value = 'dummy_path'mock_redis_client.get.return_value = Nonemock_torch_model = MagicMock() mock_load_model.return_value = mock_torch_model mock_torch_load.return_value = {'state_dict': 'dummy'}num_rows = 1200 num_bins = 100 expected_length = num_rows * num_bins mock_prediction_array = np.random.uniform(1.0, 10.0, size=expected_length)mock_torch_model.return_value.cpu.return_value.numpy.return_value.flatten.return_value = mock_prediction_arraymock_df_expanded = pd.DataFrame({ 'stockcode': ['85123A'] * num_rows, 'quantity': np.random.randint(50, 200, size=num_rows), 'unitprice': np.random.uniform(1.0, 10.0, size=num_rows), 'unitprice_min': np.random.uniform(1.0, 3.0, size=num_rows), 'unitprice_median': np.random.uniform(4.0, 6.0, size=num_rows), 'unitprice_max': np.random.uniform(8.0, 12.0, size=num_rows), })app.X_test = mock_df_expanded.drop(columns='quantity') app.preprocessor = mock_preprocessor with patch.object(pd, 'read_parquet', return_value=mock_df_expanded): response = flask_client.get('/v1/predict-price/85123A')assert response.status_code == 200 data = json.loads(response.data) assert isinstance(data, list) assert len(data) == num_bins assert data[0]['stockcode'] == '85123A' assert 'predicted_sales' in data[0]
在app_test.py
脚本中,我使用了Python unittest.mock
库中的@patch
装饰器来临时将函数和对象替换为模拟对象。
这使得测试可以在不依赖文件或S3存储等外部源的情况下运行。
在实践中,每次代码更改都需要添加这些测试,以确保代码更改不会导致错误。
3.2 配置Synk凭证用于SAST和SCA测试
对于SAST和SCA,我将使用安全平台Synk来查找和修复代码和依赖项中的漏洞。
Synk的主要目标是左移,通过尽早将安全性集成到开发工作流程中。
因此,Github Actions工作流程必须在触发构建过程之前运行Synk SAST和SCA过程。
要配置Synk凭证,请访问Synk账户页面,复制Auth Token,并将其存储在Github仓库的secrets中。
图. Synk账户页面截图
3.3 为AWS凭证设置OIDC
接下来,我将配置使用OIDC(OpenID Connect)处理AWS凭证。
OIDC是一种安全实践,通过利用与**外部身份提供商(IdP)**的联合身份验证方法,避免在环境中存储长期有效的AWS凭证。
IdP生成一个临时的、短期有效的令牌,该令牌与AWS交换以获取临时安全凭证,从而在有限的时间内授予对特定资源的访问权限。
为了使该过程正常工作,我将首先将身份提供商添加到AWS账户。
访问AWS的IAM控制台 > 身份提供商
:
- 提供商类型:选择
OpenID Connect
- 提供商URL:
https://token.actions.githubusercontent.com
- 受众:
sts.amazonaws.com
- 点击
添加提供商
。
3.4 为Github Actions配置IAM角色
接下来,我将为Github Actions添加一个IAM角色。
IAM角色是AWS中的一个安全实体,它定义了一组用于发出服务请求的权限。
为了使Github Actions能够访问项目中的必要AWS资源,IAM角色必须具有以下权限:
- 从AWS安全令牌服务(STS)检索身份:
GetCallerIdentity
- 运行AWS CodeBuild命令:
BatchGetBuilds
、BatchGetProjects
、StartBuild
- 记录CodeBuild项目:
GetLogEvents
、FilterLogEvents
、DescribeLogStreams
- 在系统管理器(SSM)参数存储中存储参数:
GetParameter
、GetParameters
、GetParametersByPath
- 检索和修改项目相关资源:
- Lambda函数:
GetFunction
、UpdateFunctionCode
、UpdateFunctionConfiguration
、InvokeFunction
- ECR:
ListImage
、DescribeImages
、DescribeRepositories
- Lambda函数:
我将这些权限配置为JSON格式的内联策略github_actions_permissions
:
IAM 控制台 > 角色 > 创建角色 > 添加权限 > 创建内联策略 > JSON > github_actions_permissions
:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "sts:GetCallerIdentity" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "codebuild:BatchGetBuilds", "codebuild:StartBuild", "codebuild:BatchGetProjects" ], "Resource": [ <ADD_CODEBUILD_PROJECT_ARN> ] }, { "Effect": "Allow", "Action": [ "logs:GetLogEvents", "logs:DescribeLogStreams", "logs:DescribeLogGroups", "logs:FilterLogEvents" ], "Resource": [ "arn:aws:logs:*:*:log-group:/aws/codebuild/*:*" ] }, { "Effect": "Allow", "Action": [ "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath" ], "Resource": [ "ADD_SSM_PARAMETER_ARN" ] }, { "Effect": "Allow", "Action": [ "lambda:GetFunction", "lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration", "lambda:InvokeFunction" ], "Resource": "<ADD_LAMBDA_FUNCTION_ARN>" }, { "Effect": "Allow", "Action": [ "ecr:ListImages", "ecr:DescribeImages", "ecr:DescribeRepositories" ], "Resource": "<ADD_ECR_ARN>" } ]
}
内联策略遵循了将安全范围限制在绝对最小的实践,如每个权限的“Resource
”字段所定义,即使某些权限已被更广泛的AWS托管策略(如AWSCodeBuildDeveloperAccess
)涵盖。
3.5 配置AWS CodeBuild
最后,我将创建一个AWS CodeBuild项目。
该过程包括:
- 步骤1. 为AWS CodeBuild添加IAM角色,
- 步骤2. 创建一个CodeBuild项目,以及
- 步骤3. 配置
buildspec.yml
文件以自定义构建过程。
3.5.1 为CodeBuild添加IAM角色
CodeBuild的IAM角色需要具有以下权限:
- 将CodePipeline连接到GitHub Actions:
UseConnection
- 在CodeBuild项目上创建日志:
CreateLogGroup
、CreateLogStream
、PutLogEvents
。 - 在SSM参数存储中存储参数:
GetParameter
、GetParameters
、GetParametersByPath
- 将对象放入S3存储桶以用于CodePipeline:
GetObject
、GetObjectVersion
、PutObject
- 检索和修改项目相关资源:
- Lambda函数:
GetFunction
、UpdateFunctionCode
、UpdateFunctionConfiguration
、InvokeFunction
- ECR:
ListImage
、DescribeImages
、DescribeRepositories
- Lambda函数:
与GitHub Actions角色类似,这些权限被定义为内联策略:
IAM 控制台 > 角色 > 创建角色 > 添加权限 > 创建内联策略 > JSON > codebuild_permissions
:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "codeconnections:UseConnection" ], "Resource": "ADD_CONNCETION_ARN" }, { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:<CODEBUILD_PROJECT_ARN>", ] }, { "Effect": "Allow", "Action": [ "ssm:PutParameter", "ssm:GetParameter", "ssm:GetParameters", "ssm:DeleteParameter", "ssm:DescribeParameters" ], "Resource": [ "ADD_SSM_ARN" ] }, { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:GetObjectVersion", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::codepipeline-us-east-1-*/*" ] }, { "Effect": "Allow", "Action": [ "lambda:UpdateFunctionCode", "lambda:GetFunction", "lambda:UpdateFunctionConfiguration" ], "Resource": "ADD_LAMBDA_FUNCTION_ARN" }, { "Effect": "Allow", "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:GetAuthorizationToken", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload", "ecr:PutImage" ], "Resource": "ADD_ECR_ARN" } ]
}
3.5.2 创建CodeBuild项目
访问开发者工具 > CodeBuild > 构建项目 > 创建构建项目
,创建一个新的CodeBuild项目:
- 项目名称:
pj-sales-pred
(或.yml
文件中指定的任何名称), - 项目类型:
默认项目
, - 源1:
Github
(按照说明将Github账户连接到CodeBuild) - 仓库:
https://github.com/<您的GITHUB账户>/<仓库名称>
- 服务角色:选择在步骤1中创建的IAM角色
- 构建规范:选择
使用构建规范文件
选项/将buildspec.yml
添加到构建命令标签
配置环境:
- 环境镜像:
托管镜像
- 操作系统:
Amazon Linux 2
- 运行时:
标准
- 镜像:
aws/codebuild/amazonlinux2-x86_64-standard:5.0
- 镜像版本:
始终使用此运行时版本的最新镜像
- 环境类型:
Linux
- 计算:
3 GB内存,2 vCPU
(BUILD_GENERAL1_SMALL) - 勾选“特权”
图. AWS CodeBuild控制台截图
3.5.3 添加buildspec
文件
最后,在项目仓库的根目录添加buildspec.yml
文件。
buildspec.yml
文件通过定义关键组件来配置CodeBuild过程:
version
:指定buildspec版本。env
:定义环境变量。phases
:定义要运行的命令:pre_build
:在主构建之前运行的命令。登录ECR并在不存在时创建仓库。build
:构建Docker镜像并打标签的主体部分。post_build
:在主构建完成后运行的命令。Docker镜像被推送到ECR。artifacts
:指定存储构建输出的文件或目录。这些工件将传递到CI/CD管道的下一阶段——部署阶段。cache
:定义要在构建之间缓存的文件或目录,以加快过程。
AWS CodeBuild会自动查找此文件并相应地执行命令。
buildspec.yml
version: 0.2phases:pre_build:commands:# login to ecr- echo "=== Pre-build Phase Started ==="- AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d':' -f5)- ECR_REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com"- aws ecr get-login-password --region $AWS_DEFAULT_REGION > /tmp/ecr_password- cat /tmp/ecr_password | docker login --username AWS --password-stdin $ECR_REGISTRY- rm /tmp/ecr_password- REPOSITORY_URI="$ECR_REGISTRY/$ECR_REPOSITORY_NAME"# use github sha or codebuild commit hash as an image tag- |if [ -n "$GITHUB_SHA" ]; thenCOMMIT_HASH=$(echo $GITHUB_SHA | cut -c 1-7)echo "Using GitHub SHA: $GITHUB_SHA"elseCOMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)echo "Using CodeBuild SHA: $CODEBUILD_RESOLVED_SOURCE_VERSION"fi- IMAGE_TAG="${COMMIT_HASH:-latest}"# store image tag in aws ssm parameter store- |aws ssm put-parameter --name "/my-app/image-tag" --value "$IMAGE_TAG" --type "String" --overwrite# create an ecr registory if not exist- |aws ecr describe-repositories --repository-names $ECR_REPOSITORY_NAME --region $AWS_DEFAULT_REGION || \aws ecr create-repository --repository-name $ECR_REPOSITORY_NAME --region $AWS_DEFAULT_REGIONbuild:commands:- echo "=== Build Phase Started ==="# build docker image- docker build -t my-app -f Dockerfile.lambda .- docker tag $ECR_REPOSITORY_NAME:latest $REPOSITORY_URI:$IMAGE_TAG- docker images | grep $ECR_REPOSITORY_NAMEpost_build:commands:- echo "=== Post-build Phase Started ==="# push the docker image to ecr- docker push ${REPOSITORY_URI}:${IMAGE_TAG}artifacts:files:- '**/*'name: ml-sales-prediction-$(date +%Y-%m-%d)cache:paths:- '/root/.cache/pip/**/*'
在GitHub仓库的推送触发GitHub Actions工作流程,并成功完成测试和构建后,CodeBuild项目现在有了构建历史:
图. CodeBuild控制台截图
这标志着build_test.yml
工作流程的结束。
4 部署工作流程
接下来,我将把部署工作流程添加到管道中。
在对构建结果进行人工审查后,容器镜像最终使用GitHub Actions工作流程部署为Lambda函数。
该过程包括:
环境设置
- 设置Python,
- 安装依赖项,
- 使用OIDC配置AWS凭证,以及
- 将Git提交SHA的缩短版本提取到
SHORT_SHA
中
部署
- 检查Lambda函数是否存在,
- 从SSM参数存储中检索最新的镜像标签,
- 如果找到镜像标签,则使用检索到的镜像更新Lambda函数,
- 如果未找到,则启动新的CodeBuild项目以重新构建容器镜像,以及
- 使用容器镜像更新Lambda函数。
验证和测试:
- 检查Lambda函数是否已更新,以及
- 测试更新后的Lambda函数。
配置更新:
- 成功测试运行后,更新Lambda函数的环境变量,以及
- 清理临时文件,确保下次运行时的干净状态。
此过程在deploy.yml
脚本中定义:
.github/workflows/deploy.yml
name: Deploy Containerized Lambdaon: workflow_dispatch: inputs: branch: description: 'The branch to deploy from' required: true default: 'develop' type: choice options: - main - develop
env: GITHUB_SHA: ${{ github.sha }}permissions: id-token: write contents: readjobs: deploy: runs-on: ubuntu-latest steps:- name: checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch }}- name: set up python uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip'- name: configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ secrets.AWS_REGION_NAME }} role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}- name: set environment variables run: | echo "SHORT_SHA=${GITHUB_SHA::8}" >> $GITHUB_ENV - name: check lambda function exists run: | aws lambda get-function --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} --region ${{ secrets.AWS_REGION_NAME }} - name: retrieve image tag and validate image id: validate_image run: | IMAGE_TAG=$(aws ssm get-parameter --name "/my-app/image-tag" --query "Parameter.Value" --output text || echo "") echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV if [[ -z "$IMAGE_TAG" ]]; then echo "has_image=false" >> $GITHUB_OUTPUT else echo "... checking for image with tag: $IMAGE_TAG" IMAGE_URI=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION_NAME }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}:${IMAGE_TAG} if aws ecr describe-images --repository-name ${{ secrets.ECR_REPOSITORY }} --image-ids imageTag=$IMAGE_TAG --region ${{ secrets.AWS_REGION_NAME }} > /dev/null 2>&1; then echo "has_image=true" >> $GITHUB_OUTPUT echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_OUTPUT else echo "has_image=false" >> $GITHUB_OUTPUT fi fi - name: update lambda function with existing image if: ${{ steps.validate_image.outputs.has_image == 'true' }} run: | aws lambda update-function-code \ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ --region ${{ secrets.AWS_REGION_NAME }} \ --image-uri ${{ steps.validate_image.outputs.IMAGE_URI }} echo "...lambda function updated with existing image ..." - name: start codebuild for container build if: ${{ steps.validate_image.outputs.has_image == 'false' }} uses: aws-actions/aws-codebuild-run-build@v1 id: codebuild with: project-name: ${{ secrets.CODEBUILD_PROJECT }} source-version-override: ${{ github.event.inputs.branch }} env-vars-for-codebuild: | [ { "name": "GITHUB_REF", "value": "refs/heads/${{ github.event.inputs.branch }}" }, { "name": "BRANCH_NAME", "value": "${{ github.event.inputs.branch }}" }, { "name": "ECR_REPOSITORY_NAME", "value": "${{ secrets.ECR_REPOSITORY }}" }, { "name": "LAMBDA_FUNCTION_NAME", "value": "${{ secrets.LAMBDA_FUNCTION_NAME }}" } ] - name: update lambda function with a new image (after build) if: ${{ steps.validate_image.outputs.has_image == 'false' }} run: | LATEST_IMAGE_URI=$(aws ecr describe-images --repository-name ${{ secrets.ECR_REPOSITORY }} --query 'sort_by(imageDetails,&imagePushedAt)[-1].imagePushedAt' | xargs -I {} aws ecr describe-images --repository-name ${{ secrets.ECR_REPOSITORY }} --query 'imageDetails[?imagePushedAt==`{}`].imageUri' --output text) if [[ -z "$LATEST_IMAGE_URI" ]]; then echo "... failed to retrieve the new image uri ..." exit 1 fi aws lambda update-function-code \ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ --region ${{ secrets.AWS_REGION_NAME }} \ --image-uri "$LATEST_IMAGE_URI" echo "... lambda function updated with newly built image ..." - name: verify lambda updates run: | CURRENT_IMAGE=$(aws lambda get-function \ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ --region ${{ secrets.AWS_REGION_NAME }} \ --query 'Code.ImageUri' \ --output text) if [[ $CURRENT_IMAGE == *"dkr.ecr"* ]]; then echo "✅ lambda function successfully updated with new image" else echo "❌ lambda function update may have failed" exit 1 fi - name: test lambda function if: github.event.inputs.branch == 'main' run: | aws lambda invoke \ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ --region ${{ secrets.AWS_REGION_NAME }} \ --payload '{"test": true}' \ --cli-binary-format raw-in-base64-out \ response.json cat response.json - name: update lambda environment variables if: github.event.inputs.branch == 'main' run: | DEPLOY_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) IMAGE_TAG="${{ env.IMAGE_TAG }}" echo "ENVIRONMENT: production" echo "VERSION: $IMAGE_TAG" echo "DEPLOY_TIME: $DEPLOY_TIME" MAX_ATTEMPTS=30 ATTEMPT=1 while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do echo "... attempt $ATTEMPT/$MAX_ATTEMPTS: checking function state ..." FUNCTION_STATE=$(aws lambda get-function \ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ --region ${{ secrets.AWS_REGION_NAME }} \ --query 'Configuration.State' \ --output text) LAST_UPDATE_STATUS=$(aws lambda get-function \ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ --region ${{ secrets.AWS_REGION_NAME }} \ --query 'Configuration.LastUpdateStatus' \ --output text) if [ "$FUNCTION_STATE" = "Active" ] && [ "$LAST_UPDATE_STATUS" = "Successful" ]; then echo "✅ function is ready for configuration update" break elif [ "$LAST_UPDATE_STATUS" = "Failed" ]; then echo "❌ function update failed" exit 1 else echo "function not ready yet, waiting 30 seconds..." sleep 30 ATTEMPT=$((ATTEMPT + 1)) fi done if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then echo "❌ Timeout waiting for function to be ready" exit 1 fi aws lambda update-function-configuration \ --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \ --region ${{ secrets.AWS_REGION_NAME }} \ --environment "Variables={ENVIRONMENT=production,VERSION=$IMAGE_TAG,DEPLOY_TIME=$DEPLOY_TIME}"- name: cleanup if: always() run: | echo "=== Cleanup ===" rm -f response.json echo "✅ cleanup completed"
deploy.yml
无需进一步配置,基础设施CI/CD管道现已集成。
在下一节中,我将配置Grafana以进行更高级的监控。
此步骤是_可选的_,因为AWS CloudWatch也可以满足您的监控需求。
5 使用Grafana进行监控
在本节中,我将配置Grafana,在AWS CloudWatch之上进行高级日志记录和监控。
Grafana是一个开源的数据可视化和分析工具。
它允许查询、可视化、告警和理解指标,无论它们存储在哪里。
配置过程包括:
- 创建IAM用户,
- 将角色和策略附加到IAM用户,以及
- 将数据源连接到Grafana。
5.1 为Grafana创建AWS IAM用户
首先,我将添加一个专门用于Grafana集成的新IAM用户,并授予它对各种AWS服务的只读访问权限。
这确保了将权限隔离到它需要访问的特定资源,遵循最小权限原则。
访问IAM 控制台 > 用户 > 创建用户 > 添加用户名称“grafana” > 直接附加策略 > 创建策略 > JSON
:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "ListAllLogGroups", "Effect": "Allow", "Action": [ "logs:DescribeLogGroups" ], "Resource": "*" }, { "Sid": "AccessSpecificLogGroups", "Effect": "Allow", "Action": [ "logs:DescribeLogStreams", "logs:GetLogEvents", "logs:FilterLogEvents", "logs:StartQuery", "logs:StopQuery", "logs:GetQueryResults", "logs:DescribeMetricFilters", "logs:GetLogGroupFields", "logs:DescribeExportTasks", "logs:DescribeDestinations" ], "Resource": [ "arn:aws:logs:*:*:log-group:/aws/lambda/*", "arn:aws:logs:*:*:log-group:/aws/codebuild/*", "arn:aws:logs:*:*:log-group:/aws/apigateway/*", "arn:aws:logs:*:*:log-group:<RDS NAME>*", "arn:aws:logs:*:*:log-group:<PROJECT NAME>*", "arn:aws:logs:*:*:log-group:application/*", "arn:aws:logs:*:*:log-group:custom/*" ] }, { "Sid": "CloudWatchLogsQueryOperations", "Effect": "Allow", "Action": [ "logs:DescribeQueries", "logs:DescribeResourcePolicies", "logs:DescribeSubscriptionFilters" ], "Resource": "*" }, { "Sid": "CloudWatchMetricsAccess", "Effect": "Allow", "Action": [ "cloudwatch:GetMetricStatistics", "cloudwatch:GetMetricData", "cloudwatch:ListMetrics", "cloudwatch:DescribeAlarms", "cloudwatch:DescribeAlarmsForMetric", "cloudwatch:GetDashboard", "cloudwatch:ListDashboards", "cloudwatch:DescribeAlarmHistory", "cloudwatch:GetMetricWidgetImage", "cloudwatch:ListTagsForResource" ], "Resource": "*" }, { "Sid": "EC2DescribeAccess", "Effect": "Allow", "Action": [ "ec2:DescribeInstances", "ec2:DescribeRegions", "ec2:DescribeTags", "ec2:DescribeAvailabilityZones", "ec2:DescribeSecurityGroups", "ec2:DescribeSubnets", "ec2:DescribeVpcs", "ec2:DescribeVolumes", "ec2:DescribeNetworkInterfaces" ], "Resource": "*" }, { "Sid": "ResourceGroupsAccess", "Effect": "Allow", "Action": [ "resource-groups:ListGroups", "resource-groups:GetGroup", "resource-groups:ListGroupResources", "resource-groups:SearchResources" ], "Resource": "*" }, { "Sid": "LambdaDescribeAccess", "Effect": "Allow", "Action": [ "lambda:ListFunctions", "lambda:GetFunction", "lambda:ListTags", "lambda:GetAccountSettings", "lambda:ListEventSourceMappings" ], "Resource": "*" }, { "Sid": "APIGatewayDescribeAccess", "Effect": "Allow", "Action": [ "apigateway:GET" ], "Resource": [ "arn:aws:apigateway:*::/restapis", "arn:aws:apigateway:*::/restapis/*/stages", "arn:aws:apigateway:*::/restapis/*/resources", "arn:aws:apigateway:*::/domainnames", "arn:aws:apigateway:*::/usageplans" ] }, { "Sid": "ECSDescribeAccess", "Effect": "Allow", "Action": [ "ecs:ListClusters", "ecs:DescribeClusters", "ecs:ListServices", "ecs:DescribeServices", "ecs:ListTasks", "ecs:DescribeTasks" ], "Resource": "*" }, { "Sid": "RDSDescribeAccess", "Effect": "Allow", "Action": [ "rds:DescribeDBInstances", "rds:DescribeDBClusters", "rds:ListTagsForResource" ], "Resource": "*" }, { "Sid": "TaggingAccess", "Effect": "Allow", "Action": [ "tag:GetResources", "tag:GetTagKeys", "tag:GetTagValues" ], "Resource": "*" }, { "Sid": "XRayAccess", "Effect": "Allow", "Action": [ "xray:BatchGetTraces", "xray:GetServiceGraph", "xray:GetTimeSeriesServiceStatistics", "xray:GetTraceSummaries" ], "Resource": "*" }, { "Sid": "SNSAccess", "Effect": "Allow", "Action": [ "sns:ListTopics", "sns:GetTopicAttributes" ], "Resource": "*" }, { "Sid": "SQSAccess", "Effect": "Allow", "Action": [ "sqs:ListQueues", "sqs:GetQueueAttributes" ], "Resource": "*" } ]
}
5.2 附加角色和策略
创建IAM用户后,我将通过访问IAM 控制台 > 策略 > 创建策略 > JSON
并添加与上一步中创建的IAM用户相同的策略来创建策略。
然后,通过访问IAM 控制台 > 角色 > 创建角色 > 自定义信任策略
来配置新角色:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<AWS ACCOUNT ID>:user/grafana" }, "Action": "sts:AssumeRole" } ]
}
然后,附加在上一步中创建的策略。
IAM角色信任策略定义了谁可以承担特定的IAM角色,例如“谁被信任使用此角色的权限?”
我配置了信任策略,允许在第一步中创建的Grafana IAM用户grafana
承担该角色。
5.3 将数据源连接到Grafana
最后,我将为Grafana配置数据源。
访问Grafana 控制台 > 数据源 > cloudwatch > 添加
:
- 访问密钥ID:IAM用户
grafana
的访问密钥ID - 秘密访问密钥:IAM用户
grafana
的秘密访问密钥 - 承担角色ARN:上一步中创建的IAM角色的ARN。
- 点击
保存并测试
。
这将允许从相关AWS资源导入数据:
图. Grafana控制台截图
这标志着使用Grafana进行监控的过程的结束。
所有代码都可以在我的GitHub仓库中找到。
6 结论
在本文中,我们演示了如何将健壮的CI/CD管道集成到机器学习应用中。
尽管使用的具体服务可能因项目需求而异,但其原则保持不变:自动化流程并尽早发现错误,避免实际部署。