最初的痛点源于混乱。我们的数据科学团队负责产出模型,而平台团队负责将这些模型封装成服务并部署。问题在于,我们的基础设施横跨三大公有云:AWS、Azure 和 Google Cloud。每个云都有自己的一套 Serverless 方案——Lambda、Azure Functions、Cloud Functions——以及各自迥异的部署工具链和权限体系。结果就是,同一个模型,需要三套部署脚本、三套CI/CD流水线、三套独立的监控告警。任何微小的逻辑更新都意味着三倍的工作量和三倍的出错风险。
我们急需一个统一的、声明式的发布流程,以代码(Git)为唯一信源,实现一次变更、同步部署到所有目标环境。目标很明确:数据科学家只需提交他们的模型和处理代码,剩下的构建、打包、基建配置和多云部署应该完全自动化。这正是 GitOps 的核心思想。
初步构想是,将 AI 推理应用容器化,以此作为跨平台交付的原子单元。GitHub 作为代码仓库和 CI/CD 执行器,而 Terraform 则负责抹平三大云厂商在基础设施层面的差异。整个流程应该像这样:
graph TD A[开发者 Push 代码到 GitHub] --> B{GitHub Actions 触发}; B --> C[构建 Docker 镜像 & 推送到 GHCR]; C --> D{执行 Terraform}; D --> E[在 AWS 创建/更新 Lambda]; D --> F[在 Azure 创建/更新 Function App]; D --> G[在 Google Cloud 创建/更新 Cloud Function]; subgraph GitHub A B C D end subgraph 云平台 E F G end
这个方案的核心是三点:
- 应用标准化:通过 Docker 容器封装 Python 应用及其所有依赖,确保环境一致性。
- 基础设施代码化:使用 Terraform 定义所有云平台的 Serverless 函数资源,实现一份代码管理多云基建。
- 流程自动化:GitHub Actions 作为中枢,串联代码变更、镜像构建和基础设施变更。
项目结构与核心代码
为了实现这个目标,我们规划了如下的项目目录结构。这种结构将应用代码、基础设施代码和 CI/CD 流水线清晰地分离。
.
├── .github
│ └── workflows
│ └── deploy.yml # GitHub Actions 主流程
├── src
│ ├── app.py # Flask 应用,我们的 AI 推理服务
│ ├── Dockerfile # 应用容器化配置
│ └── requirements.txt # Python 依赖
└── terraform
├── main.tf # Terraform 主入口和 Provider 配置
├── variables.tf # 输入变量定义
├── outputs.tf # 输出定义
├── aws.tf # AWS Lambda 相关资源
├── azure.tf # Azure Functions 相关资源
└── gcp.tf # Google Cloud Functions 相关资源
1. AI 推理服务 (src/app.py
)
这不仅仅是一个 “Hello World”。我们模拟一个真实的数据科学场景:一个简单的 scikit-learn 模型推理服务。它接收 JSON 格式的输入,执行预测,并返回结果。关键在于它包含了日志记录、配置管理和基本的错误处理,这是生产级代码的底线。
# src/app.py
import os
import logging
import traceback
from flask import Flask, request, jsonify
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = Flask(__name__)
# 模拟一个预加载的模型。在真实项目中,这里会从 GCS/S3/Blob Storage 加载模型文件。
# 为了示例简单,我们用一个简单的函数代替。
def predict_model(data):
"""
一个模拟的模型预测函数。
它接收一个包含 'features' 键的字典。
简单地将所有特征值求和作为预测结果。
"""
if not isinstance(data, dict) or "features" not in data:
raise ValueError("输入必须是包含 'features' 键的字典")
if not all(isinstance(x, (int, float)) for x in data["features"]):
raise ValueError("'features' 列表必须只包含数值")
return sum(data["features"])
@app.route('/', methods=['GET'])
def health_check():
"""健康检查端点,用于云平台的存活探测。"""
return jsonify({"status": "ok"}), 200
@app.route('/predict', methods=['POST'])
def predict():
"""
主预测端点。
期望的 JSON body: {"features": [1, 2, 3, 4]}
"""
try:
if not request.is_json:
return jsonify({"error": "请求必须是 JSON 格式"}), 400
data = request.get_json()
logging.info(f"接收到预测请求: {data}")
prediction = predict_model(data)
response = {
"prediction_id": os.urandom(8).hex(),
"result": prediction
}
logging.info(f"预测完成: {response}")
return jsonify(response), 200
except ValueError as ve:
logging.warning(f"输入数据校验失败: {str(ve)}")
return jsonify({"error": f"无效输入: {str(ve)}"}), 400
except Exception as e:
logging.error(f"预测过程中发生未知错误: {traceback.format_exc()}")
return jsonify({"error": "内部服务器错误"}), 500
# Serverless 平台通常需要一个可调用的应用实例
# Gunicorn 通常用于生产环境,但对于某些平台,直接运行 Flask 开发服务器已足够
if __name__ == '__main__':
# 从环境变量获取端口,这是云原生应用的通用实践
port = int(os.environ.get("PORT", 8080))
app.run(host='0.0.0.0', port=port)
2. 容器化 (src/Dockerfile
)
Dockerfile 的设计至关重要。我们采用多阶段构建来减小最终镜像的体积,这直接影响冷启动时间和存储成本。
# src/Dockerfile
# --- 阶段 1: 构建阶段 ---
# 使用一个包含完整构建工具链的镜像
FROM python:3.9-slim as builder
WORKDIR /app
# 优化 Docker 缓存:仅当 requirements.txt 变化时才重新安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# --- 阶段 2: 运行阶段 ---
# 使用一个非常精简的基础镜像
FROM python:3.9-slim
WORKDIR /app
# 从构建阶段复制已安装的依赖
COPY /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY /usr/local/bin /usr/local/bin
# 复制应用代码
COPY app.py .
# 暴露端口,供云平台运行时使用
EXPOSE 8080
# 设置环境变量,确保 Python 日志能立即输出
ENV PYTHONUNBUFFERED=1
# 启动命令。Gunicorn 是生产级的 WSGI 服务器。
# AWS Lambda 和 Google Cloud Functions (Gen2) 能很好地处理这种基于 HTTP 服务器的容器。
# Azure Functions 需要特定的适配器,但也能通过自定义容器支持。
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
这里的 requirements.txt
可能包含:
Flask==2.2.3
gunicorn==20.1.0
scikit-learn==1.2.2 # 示例依赖
numpy==1.24.2
基础设施代码化:Terraform 的力量
Terraform 让我们能够用同一种语言描述三个完全不同的云平台。这里的挑战在于,尽管目标都是“运行一个容器”,但每个平台的实现细节和所需资源天差地别。
Terraform 主配置 (terraform/main.tf
)
这里配置了所有云的 Provider,并为 GitHub Actions OIDC 认证设置了必要的凭据来源。
# terraform/main.tf
terraform {
required_version = ">= 1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.60"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.40"
}
google = {
source = "hashicorp/google"
version = "~> 4.50"
}
}
}
# --- Provider 配置 ---
# GitHub Actions 会通过 OIDC 注入临时凭证
provider "aws" {
region = var.aws_region
}
provider "azurerm" {
features {}
}
provider "google" {
project = var.gcp_project_id
region = var.gcp_region
}
# --- 变量定义 ---
# terraform/variables.tf
variable "image_uri" {
description = "要部署的 Docker 镜像的完整 URI (带 tag)"
type = string
}
variable "aws_region" {
description = "AWS 区域"
type = string
default = "us-east-1"
}
# ... 其他 Azure 和 GCP 的变量 ...
AWS Lambda 资源 (terraform/aws.tf
)
AWS Lambda 支持容器镜像。我们需要创建一个 IAM Role 赋予 Lambda 执行权限,以及 Lambda 函数本身。
# terraform/aws.tf
resource "aws_iam_role" "lambda_exec_role" {
name = "lambda-multi-cloud-exec-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_policy" {
role = aws_iam_role.lambda_exec_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "ai_function" {
function_name = "multi-cloud-ai-function-aws"
role = aws_iam_role.lambda_exec_role.arn
package_type = "Image"
image_uri = var.image_uri
timeout = 30
memory_size = 512
image_config {
command = ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
}
# Lambda URL 用于直接通过 HTTP 调用
depends_on = [aws_iam_role_policy_attachment.lambda_policy]
}
resource "aws_lambda_function_url" "ai_function_url" {
function_name = aws_lambda_function.ai_function.function_name
authorization_type = "NONE" # 为了演示方便,设为公开访问
}
output "aws_lambda_endpoint" {
value = aws_lambda_function_url.ai_function_url.function_url
}
一个常见的坑:Lambda 的执行角色必须有权限从 ECR(或我们案例中的 GHCR)拉取镜像。如果使用 ECR,需要 AmazonEC2ContainerRegistryReadOnly
策略。由于我们用 GHCR,Lambda 运行时默认可以拉取公开镜像,如果是私有则需要配置。
Azure Functions 资源 (terraform/azure.tf
)
Azure Functions 运行容器需要一个 App Service Plan(即使是 Consumption Plan)、一个存储账户和 Function App 本身。
# terraform/azure.tf
resource "azurerm_resource_group" "rg" {
name = "rg-multicloud-ai-functions"
location = var.azure_location
}
resource "azurerm_storage_account" "sa" {
name = "samulticloudai${random_string.unique.result}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_service_plan" "plan" {
name = "plan-multicloud-ai"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
os_type = "Linux"
sku_name = "Y1" # Dynamic SKU for Consumption plan
}
resource "azurerm_linux_function_app" "ai_function" {
name = "func-multicloud-ai-${random_string.unique.result}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
service_plan_id = azurerm_service_plan.plan.id
storage_account_name = azurerm_storage_account.sa.name
storage_account_access_key = azurerm_storage_account.sa.primary_access_key
site_config {
application_stack {
docker_image = var.image_uri
docker_image_tag = split(":", var.image_uri)[1]
}
always_on = false
}
app_settings = {
"WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
"DOCKER_REGISTRY_SERVER_URL" = "https://ghcr.io"
# 如果 GHCR 镜像是私有的,需要在此配置用户名和密码(PAT)
}
}
resource "random_string" "unique" {
length = 6
special = false
upper = false
}
output "azure_function_endpoint" {
value = "https://${azurerm_linux_function_app.ai_function.default_hostname}/predict"
}
Google Cloud Functions 资源 (terraform/gcp.tf
)
Google Cloud Functions (2nd gen) 基于 Cloud Run 和 Eventarc,原生支持容器。
# terraform/gcp.tf
# 确保必要的 API 已启用
resource "google_project_service" "apis" {
for_each = toset([
"cloudfunctions.googleapis.com",
"cloudbuild.googleapis.com",
"run.googleapis.com",
"artifactregistry.googleapis.com"
])
project = var.gcp_project_id
service = each.key
disable_on_destroy = false
}
resource "google_cloudfunctions2_function" "ai_function" {
name = "multi-cloud-ai-function-gcp"
location = var.gcp_region
project = var.gcp_project_id
build_config {
runtime = "python39" # 这里的 runtime 不重要,因为我们用 Docker
entry_point = "app" # 同上
source {
storage_source {
# 我们是预构建镜像,所以这里留空
}
}
}
service_config {
max_instance_count = 3
min_instance_count = 0
available_memory = "512Mi"
timeout_seconds = 60
ingress_settings = "ALLOW_ALL" # 公开访问
# 关键部分:指定预构建的容器镜像
service_config {
docker_repository {
uri = var.image_uri
}
}
}
depends_on = [google_project_service.apis]
}
output "gcp_function_endpoint" {
value = google_cloudfunctions2_function.ai_function.service_config[0].uri
}
这里的实现有一个细节:Google Cloud Function 的 Terraform Provider 在处理预构建容器时,其配置方式 (service_config
嵌套) 与直接从源码构建不同,很容易配错。
自动化中枢:GitHub Actions 工作流
这是将所有部分粘合在一起的胶水。.github/workflows/deploy.yml
定义了从代码提交到多云部署的完整流程。
# .github/workflows/deploy.yml
name: Deploy Multi-Cloud AI Function
on:
push:
branches:
- main
workflow_dispatch:
env:
# GHCR 镜像仓库路径
IMAGE_NAME: ${{ github.repository_owner }}/multi-cloud-ai-function
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # 写入 GHCR 的权限
outputs:
image_uri: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ghcr.io/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=,format=short
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: ./src
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy-infrastructure:
runs-on: ubuntu-latest
needs: build-and-push-image
permissions:
id-token: write # OIDC 认证的关键权限
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
# --- AWS OIDC 配置 ---
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsDeployRole # 需要预先在 AWS 创建此角色
aws-region: ${{ vars.AWS_REGION }}
# --- GCP OIDC 配置 ---
- name: Authenticate to Google Cloud
id: 'auth'
uses: 'google-github-actions/auth@v1'
with:
workload_identity_provider: 'projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: 'github-actions-deploy-sa@${{ secrets.GCP_PROJECT_ID }}.iam.gserviceaccount.com' # 需要预先创建 WIF 和 SA
# --- Azure OIDC 配置 ---
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.3.7
- name: Terraform Init
id: init
run: terraform init
working-directory: ./terraform
- name: Terraform Plan
id: plan
run: terraform plan -var="image_uri=${{ needs.build-and-push-image.outputs.image_uri }}" -out=tfplan
working-directory: ./terraform
- name: Terraform Apply
id: apply
run: terraform apply -auto-approve tfplan
working-directory: ./terraform
这个工作流有几个关键点:
- OIDC 认证:我们为每个云平台都配置了 OpenID Connect。这避免了在 GitHub Secrets 中存储长期有效的静态密钥,是目前最安全、最推荐的 CI/CD 认证方式。
- 作业依赖:
deploy-infrastructure
作业依赖于build-and-push-image
,确保只有在镜像成功构建并推送后才进行部署。 - 输出传递:构建作业通过
outputs
将带有唯一 SHA 标签的镜像 URI 传递给部署作业,确保部署的是本次构建的确切产物。
方案的局限性与未来迭代路径
这套体系解决了多云部署的一致性和自动化问题,但在真实生产环境中,它并非银弹。
首先,冷启动问题依然存在。尽管我们优化了镜像大小,但 Serverless 平台的冷启动延迟对于需要极低响应时间的 AI 服务可能仍然无法接受。未来的优化方向可能是为每个平台配置预置并发(Provisioned Concurrency in AWS, Premium Plan in Azure, Min Instances in GCP),但这会显著增加成本。
其次,基础设施的差异性并未完全消除。Terraform 只是提供了一个统一的配置语言,但每个云平台的资源(如 aws_lambda_function
vs azurerm_linux_function_app
)及其参数仍然是特定于平台的。这意味着维护这套 Terraform 代码仍然需要对三个云都有深入了解。一个可能的演进是构建更高层次的内部抽象,例如使用 Crossplane 或开发一个内部平台,让开发者只需定义一个更通用的 “AI Service” 对象。
最后,可观测性是分散的。应用日志和性能指标被发送到各自的云平台(CloudWatch, Azure Monitor, Google Cloud’s operations suite)。要获得统一的视图,需要引入一个聚合层,例如通过 OpenTelemetry 标准将遥测数据统一发送到像 Datadog 或 Grafana 这样的第三方平台。这会是下一步至关重要的工作,否则故障排查将再次陷入跨平台切换的泥潭。