基于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管道](#1 什么是CI/CD管道)
  • [2 工作流程实战](#2 工作流程实战)
  • [3 测试与构建工作流程](#3 测试与构建工作流程)
    • [3.1 添加PyTest脚本](#3.1 添加PyTest脚本)
    • [3.2 配置Synk凭证用于SAST和SCA测试](#3.2 配置Synk凭证用于SAST和SCA测试)
    • [3.3 为AWS凭证设置OIDC](#3.3 为AWS凭证设置OIDC)
    • [3.4 为Github Actions配置IAM角色](#3.4 为Github Actions配置IAM角色)
    • [3.5 配置AWS CodeBuild](#3.5 配置AWS CodeBuild)
      • [3.5.1 为CodeBuild添加IAM角色](#3.5.1 为CodeBuild添加IAM角色)
      • [3.5.2 创建CodeBuild项目](#3.5.2 创建CodeBuild项目)
      • [3.5.3 添加`buildspec`文件](#3.5.3 添加buildspec文件)
  • [4 部署工作流程](#4 部署工作流程)
  • [5 使用Grafana进行监控](#5 使用Grafana进行监控)
    • [5.1 为Grafana创建AWS IAM用户](#5.1 为Grafana创建AWS IAM用户)
    • [5.2 附加角色和策略](#5.2 附加角色和策略)
    • [5.3 将数据源连接到Grafana](#5.3 将数据源连接到Grafana)
  • [6 结论](#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

yaml 复制代码
name: Build and Test

on:  
  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: write

jobs:  
  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脚本

python 复制代码
import os  
import shutil  
import numpy as np  
import pytest

import src.main as main_script

def 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应用脚本

python 复制代码
import os  
import json  
import io  
import pandas as pd  
import numpy as np  
from unittest.mock import patch, MagicMock

  
import app

  
os.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 = None

      
    mock_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_array

      
    mock_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

json 复制代码
{  
 "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

json 复制代码
{  
 "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

yaml 复制代码
version: 0.2

phases:
  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" ]; then
          COMMIT_HASH=$(echo $GITHUB_SHA | cut -c 1-7)
          echo "Using GitHub SHA: $GITHUB_SHA"
        else
          COMMIT_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_REGION
  
  build:
    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_NAME

  post_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

yaml 复制代码
name: Deploy Containerized Lambda

on:  
  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: read

jobs:  
  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

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 控制台 > 角色 > 创建角色 > 自定义信任策略来配置新角色:

json 复制代码
{  
    "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管道集成到机器学习应用中。

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

相关推荐
红苕稀饭6662 小时前
VideoChat-Flash论文阅读
人工智能·深度学习·机器学习
周杰伦_Jay2 小时前
【图文详解】强化学习核心框架、数学基础、分类、应用场景
人工智能·科技·算法·机器学习·计算机视觉·分类·数据挖掘
炒香菇的书呆子2 小时前
基于Amazon S3设置AWS Transfer Family Web 应用程序
javascript·aws
jie*3 小时前
小杰机器学习(six)——概率论——1.均匀分布2.正态分布3.数学期望4.方差5.标准差6.多维随机变量及其分布
人工智能·机器学习·概率论
偶尔贪玩的骑士3 小时前
Machine Learning HW4 report: 语者识别 (Hongyi Lee)
人工智能·深度学习·机器学习·self-attention
没有口袋啦3 小时前
《机器学习与深度学习》入门
人工智能·深度学习·机器学习
debug 小菜鸟4 小时前
aws 实战小bug
云计算·bug·aws
cocosgirl5 小时前
AWS Quicksight实践:从零到可视化分析
aws
danns8885 小时前
aws用ami新创建之后用密码登录不了
云计算·aws