当前位置: 首页 > news >正文

基于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相关配置:
    1. 为AWS凭证设置OIDC,
    2. 为GitHub Actions定义IAM角色,以及
    3. 配置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
  • 提供商URLhttps://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命令BatchGetBuildsBatchGetProjectsStartBuild
  • 记录CodeBuild项目GetLogEventsFilterLogEventsDescribeLogStreams
  • 在系统管理器(SSM)参数存储中存储参数GetParameterGetParametersGetParametersByPath
  • 检索和修改项目相关资源
    • Lambda函数:GetFunctionUpdateFunctionCodeUpdateFunctionConfigurationInvokeFunction
    • ECR:ListImageDescribeImagesDescribeRepositories

我将这些权限配置为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 ActionsUseConnection
  • 在CodeBuild项目上创建日志CreateLogGroupCreateLogStreamPutLogEvents
  • 在SSM参数存储中存储参数GetParameterGetParametersGetParametersByPath
  • 将对象放入S3存储桶以用于CodePipeline:GetObjectGetObjectVersionPutObject
  • 检索和修改项目相关资源
    • Lambda函数:GetFunctionUpdateFunctionCodeUpdateFunctionConfigurationInvokeFunction
    • ECR:ListImageDescribeImagesDescribeRepositories

与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文件中指定的任何名称),
  • 项目类型默认项目
  • 源1Github(按照说明将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管道集成到机器学习应用中。

尽管使用的具体服务可能因项目需求而异,但其原则保持不变:自动化流程并尽早发现错误,避免实际部署。

http://www.dtcms.com/a/395111.html

相关文章:

  • 3D 文件格式解释
  • 对Hive表进行归档,减少小文件的影响
  • R 中,geo 数据集 分析探针转基因的时候,一个探针对应的多个基因的情况
  • 机器学习-逻辑回归-考试预测通过-1
  • 计算机中用8位如何计算最大值和最小值-128~127
  • PyTorch 神经网络工具箱完全指南
  • docker一键安装部署若依Ruoyi-Vue(保姆级)
  • 通义DeepResearch论文六连发全面解读
  • 大模型应用-prompt提示词工程
  • Windows 命令行:使用路径名和文件名来启动文件
  • 稻瘟病监测仪的功能用途
  • 仿照豆包实现 Prompt 变量模板输入框
  • 如何安装 SQLPro Studio for Mac?v2024.21.dmg 文件安装步骤详解(附安装包)
  • 扣子空间:字节跳动推出的AI Agent 智能体平台
  • 编程基础:表驱动
  • 内网穿透的应用-RemoteJVMDebug+cpolar:内网服务器调试的无界解决方案
  • 如何将PPT每一页批量导出为高清JPG图片?一文讲清操作流程
  • 高防服务器如何实现安全防护?ddos攻击会暴露ip吗?
  • linux硬盘分区管理
  • spring boot实现MCP服务器,及其cursor测试使用的方法
  • web前端开发与服务器通信的技术变迁历程
  • 市值机器人:智能力量与监管博弈下的金融新生态
  • LeetCode:46.二叉树展开为链表
  • LeetCode算法日记 - Day 50: 汉诺塔、两两交换链表中的节点
  • 力扣每日一刷Day24
  • LeetCode 226. 翻转二叉树
  • leetcode 2331 计算布尔二叉树的值
  • docker: Error response from daemon: Get “https://registry-1.docker.io/v2/“
  • 从50ms到30ms:YOLOv10部署中图像预处理的性能优化实践
  • 6. Typescript 类型体操