使用AWS ECS和Lambda构建无服务器图像处理流水线

本文详细介绍了如何使用AWS ECS和Lambda构建完整的无服务器图像处理流水线,包括S3存储桶配置、Lambda函数编写、API Gateway设置、前端应用开发和ECS集群部署,涵盖了完整的技术实现方案。

无服务器图像处理流水线:使用AWS ECS和Lambda

欢迎来到开发和自动化的世界!今天我们将深入探讨一个令人兴奋的项目:使用AWS服务构建无服务器图像处理流水线。

项目从创建S3存储桶开始,用于存储上传的图像和处理后的缩略图,最终使用Lambda、API Gateway(用于触发Lambda函数)、DynamoDB(存储图像元数据)等多种服务,最后通过创建项目的Docker镜像在ECS集群中运行该程序。

这个项目包含了丰富的云服务和开发技术栈,如Next.js,实践这个项目将进一步提升您对云服务及其相互交互方式的理解。事不宜迟,让我们开始吧!

注意:本文中的代码和说明仅用于演示和学习目的。生产环境需要对配置和安全性进行更严格的控制。

先决条件

在开始项目之前,我们需要确保系统满足以下要求:

  • AWS账户:由于我们使用AWS服务,需要一个AWS账户。建议配置具有所需服务访问权限的IAM用户
  • AWS服务基础理解:需要了解S3(用于存储)、API Gateway(触发Lambda函数)等服务
  • Node.js安装:前端使用Next.js构建,因此需要安装Node.js

代码参考请查看GitHub仓库。

AWS服务设置

我们将从设置AWS服务开始项目。首先创建两个S3存储桶:sample-image-uploads-bucketsample-thumbnails-bucket。名称较长的原因是存储桶名称必须在整个AWS工作空间中唯一。

创建存储桶:转到S3仪表板,点击"创建存储桶",选择"通用目的",命名(sample-image-uploads-bucket),其余配置保持默认。

同样创建另一个名为sample-thumbnails-bucket的存储桶,但在此存储桶中确保取消选中"阻止公共访问",因为ECS集群需要此权限。

我们需要确保sample-thumbnails-bucket具有公共读取权限,以便ECS前端可以显示图像。为此,我们将以下策略附加到该存储桶:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::sample-thumbnails-bucket/*"
    }
  ]
}

创建存储桶后,让我们转到数据库以存储图像元数据。我们将创建一个DynamoDB表。转到DynamoDB控制台,点击"创建表",命名(image_metadata),在主键中选择字符串,命名为image_id

AWS服务需要相互通信,因此需要具有适当权限的角色。要创建角色,请转到IAM仪表板,选择"角色",点击"创建角色"。在信任身份类型下选择"AWS服务",在使用案例中选择"Lambda"。附加以下策略:

  • AmazonS3FullAccess
  • AmazonDynamoDBFullAccess
  • CloudWatchLogsFullAccess

为此角色命名(Lambda-Image-Processor-Role)并保存。

创建Lambda函数

我们已经准备好了Lambda角色、存储桶和DynamoDB表,现在让我们创建Lambda函数来处理图像并生成缩略图。由于我们使用Pillow库处理图像,而Lambda默认不提供该库。要解决此问题,我们将在Lambda函数中添加一个层。

转到Lambda仪表板,点击"创建函数"。选择"从头开始创作",选择Python 3.9作为运行时语言,命名:image-processor。在代码选项卡中,选择"从选项上传",选择zip文件,上传image-processor的zip文件。

转到配置,在权限列下,通过将现有角色更改为我们创建的Lambda-Image-Processor-Role来编辑配置。

现在转到S3存储桶(sample-image-uploads-bucket),转到其属性部分,向下滚动到"事件通知",点击"创建事件通知",命名(trigger-image-processor),在事件类型中选择"PUT",选择我们创建的Lambda函数(image-processor)。

由于Pillow不随Lambda库内置,我们将执行以下步骤来解决此问题:

转到Lambda函数(image-processor),向下滚动到"层"部分,点击"添加层"。

在添加层部分,选择"指定ARN"并提供此ARN:arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p39-pillow:1。根据情况更改区域;我使用us-east-1。添加层。

现在在Lambda函数的代码选项卡中,您将拥有一个lambda_function.py文件,将以下内容放入其中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import boto3
import uuid
import os
from PIL import Image
from io import BytesIO
import datetime

s3 = boto3.client('s3')
dynamodb = boto3.client('dynamodb')

UPLOAD_BUCKET = '<YOUR_BUCKET_NAME>'  # 确保更改此值
THUMBNAIL_BUCKET = '<YOUR_BUCKET_NAME>' # 确保更改此值
DDB_TABLE = 'image_metadata'

def lambda_handler(event, context):
    # 获取上传的图像详情
    record = event['Records'][0]
    bucket = record['s3']['bucket']['name']
    key = record['s3']['object']['key']

    # 下载图像
    response = s3.get_object(Bucket=bucket, Key=key)
    image = Image.open(BytesIO(response['Body'].read()))

    # 生成缩略图
    image.thumbnail((200, 200))

    # 在内存中保存缩略图
    thumbnail_buffer = BytesIO()
    image.save(thumbnail_buffer, 'JPEG')
    thumbnail_buffer.seek(0)

    # 上传缩略图到S3
    thumbnail_key = f"thumb_{key}"
    s3.put_object(
        Bucket=THUMBNAIL_BUCKET,
        Key=thumbnail_key,
        Body=thumbnail_buffer,
        ContentType='image/jpeg'
    )

    # 在DynamoDB中存储元数据
    image_id = str(uuid.uuid4())
    original_url = f"https://{UPLOAD_BUCKET}.s3.amazonaws.com/{key}"
    thumbnail_url = f"https://{THUMBNAIL_BUCKET}.s3.amazonaws.com/{thumbnail_key}"
    uploaded_at = datetime.datetime.now().isoformat()

    dynamodb.put_item(
        TableName=DDB_TABLE,
        Item={
            'image_id': {'S': image_id},
            'original_url': {'S': original_url},
            'thumbnail_url': {'S': thumbnail_url},
            'uploaded_at': {'S': uploaded_at}
        }
    )

    return {
        'statusCode': 200,
        'body': f"缩略图已创建: {thumbnail_url}"
    }

现在,我们需要另一个用于API Gateway的Lambda函数,因为这将作为我们前端ECS应用程序从DynamoDB获取图像数据的入口点。

要创建Lambda函数,请转到Lambda仪表板,点击"创建函数",选择"从头开始创作"和python 3.9作为运行时,命名:get-image-metadata。在配置中,选择我们分配给其他Lambda函数的相同角色(Lambda-Image-Processor-Role)。

现在,在函数的代码部分,放入以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import boto3
import json

dynamodb = boto3.client('dynamodb')
TABLE_NAME = 'image_metadata'

def lambda_handler(event, context):
    try:
        # 扫描整个表(为简单起见,无分页)
        response = dynamodb.scan(TableName=TABLE_NAME)

        # 将DynamoDB项转换为JSON格式
        images = []
        for item in response['Items']:
            images.append({
                'image_id': item['image_id']['S'],
                'original_url': item['original_url']['S'],
                'thumbnail_url': item['thumbnail_url']['S'],
                'uploaded_at': item['uploaded_at']['S']
            })

        return {
            'statusCode': 200,
            'headers': {
                "Content-Type": "application/json"
            },
            'body': json.dumps(images)
        }

    except Exception as e:
        return {
            'statusCode': 500,
            'body': f"错误: {str(e)}"
        }

创建API Gateway

API Gateway将作为ECS前端应用程序从DynamoDB获取图像数据的入口点。它将连接到查询DynamoDB并返回图像元数据的Lambda函数。网关的URL在我们的前端应用程序中用于显示图像。

要创建API Gateway,请执行以下步骤:

转到AWS管理控制台 → 搜索"API Gateway" → 点击"创建API"。

选择"HTTP API"。 点击"构建"。 API名称:image-gallery-api 添加集成:选择"Lambda"并选择get_image_metadata函数 选择方法:GET和路径:/images 端点类型:区域 点击"下一步"并创建API Gateway URL。

创建前端应用

为了简单起见,我们将使用Next.js创建一个最小化的简单图库前端,将其Docker化,并部署在ECS上。

初始化

1
2
3
4
npx create-next-app@latest image-gallery
cd image-gallery
npm install
npm install axios

创建图库组件 创建新文件components/Gallery.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
'use client';

import { useState, useEffect } from 'react';
import axios from 'axios';
import styles from './Gallery.module.css';

const Gallery = () => {
  const [images, setImages] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchImages = async () => {
      try {
        const response = await axios.get('https://<YOUR_API_GATEWAY_INVOKE_URL>/images');
        setImages(response.data);
        setLoading(false);
      } catch (error) {
        console.error('获取图像时出错:', error);
        setLoading(false);
      }
    };

    fetchImages();
  }, []);

  if (loading) {
    return <div className={styles.loading}>加载中...</div>;
  }

  return (
    <div className={styles.gallery}>
      {images.map((image) => (
        <div key={image.image_id} className={styles.imageCard}>
          <img
            src={image.thumbnail_url}
            alt="图库缩略图"
            width={200}
            height={150}
            className={styles.thumbnail}
          />
          <p className={styles.date}>
            {new Date(image.uploaded_at).toLocaleDateString()}
          </p>
        </div>
      ))}
    </div>
  );
};

export default Gallery;

确保将Gateway-URL更改为您的API_GATEWAY_URL

添加CSS模块 创建components/Gallery.module.css

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}
.imageCard {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  overflow: hidden;
  transition: transform 0.2s;
}
.imageCard:hover {
  transform: scale(1.05);
}
.thumbnail {
  width: 100%;
  height: 150px;
  object-fit: cover;
}
.date {
  text-align: center;
  padding: 10px;
  margin: 0;
  font-size: 0.9em;
  color: #666;
}
.loading {
  text-align: center;
  padding: 50px;
  font-size: 1.2em;
}

更新主页 修改app/page.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Gallery from '../components/Gallery';

export default function Home() {
  return (
    <main>
      <h1 style={{ textAlign: 'center', padding: '20px' }}>图像图库</h1>
      <Gallery />
    </main>
  );
}

使用Next.js内置的Image组件 要使用Next.js内置的Image组件进行更好的优化,更新next.config.mjs

1
2
3
4
5
6
7
8
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['sample-thumbnails-bucket.s3.amazonaws.com'],
  },
};

export default nextConfig;

运行应用程序

1
npm run dev

在浏览器中访问http://localhost:3000,您将看到应用程序运行并显示所有上传的缩略图。

容器化和创建ECS集群

现在我们几乎完成了项目,我们将继续创建项目的Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 使用官方Node.js镜像作为基础
FROM node:18-alpine AS builder

# 设置工作目录
WORKDIR /app

# 复制包文件并安装依赖项
COPY package.json package-lock.json ./
RUN npm install

# 复制其余应用程序代码
COPY . .

# 构建Next.js应用
RUN npm run build

# 为生产使用轻量级Node.js镜像
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

# 从构建阶段复制构建的文件
COPY --from=builder /app ./

# 暴露端口
EXPOSE 3000

# 运行应用程序
CMD ["npm", "start"]

现在我们将使用以下命令构建Docker镜像:

1
docker build -t sample-nextjs-app .

现在我们有了Docker镜像,我们将将其推送到AWS ECR仓库:

步骤1:将Docker镜像推送到Amazon ECR

转到AWS管理控制台 → 搜索"ECR"(弹性容器注册表)→ 打开ECR。 创建新仓库:

  • 点击"创建仓库"
  • 设置仓库名称(例如sample-nextjs-app
  • 选择私有(或根据需要选择公共)
  • 点击"创建仓库"

将Docker镜像推送到ECR:

  • 在新创建的仓库中,点击"查看推送命令"
  • 按照命令:
    • 使用ECR验证Docker
    • 构建、标记和推送镜像 此步骤需要配置AWS CLI

步骤2:创建ECS集群

1
aws ecs create-cluster --cluster-name sample-ecs-cluster

步骤3:创建任务定义

在ECS控制台中,转到"任务定义"。 点击"创建新任务定义"。 选择"Fargate" → 点击"下一步"。 设置任务定义详情:

  • 名称:sample-nextjs-task
  • 任务角色:ecsTaskExecutionRole(如果缺少则创建一个)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Statement1",
      "Effect": "Allow",
      "Action": [
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource": "arn:aws:ecr:us-east-1:624448302051:repository/sample-nextjs-app"
    }
  ]
}

任务内存和CPU:选择适当的值(例如512MB和256 CPU)。

定义容器:

  • 点击"添加容器"
  • 容器名称:sample-nextjs-container
  • 镜像URL:粘贴来自步骤1的ECR镜像URI
  • 端口映射:为容器和主机端口都设置3000
  • 点击"添加"

点击"创建"。

步骤4:创建ECS服务

转到"ECS" → 点击"集群" → 选择您的集群(sample-ecs-cluster)。 点击"创建服务"。 选择"Fargate" → 点击"下一步"。 设置服务:

  • 任务定义:选择sample-nextjs-task
  • 集群:sample-ecs-cluster
  • 服务名称:sample-nextjs-service
  • 任务数量:1(以后可以扩展)

网络设置:

  • 选择现有VPC
  • 选择公共子网
  • 启用自动分配公共IP

点击"下一步" → “创建服务”。

步骤5:访问应用程序

转到ECS > 集群 > sample-ecs-cluster。 点击"任务"选项卡。 点击运行中的任务。 在网络下找到公共IP。

在浏览器中打开:http://<TASK_PUBLIC_IP>:3000 您的Next.js应用应该已经上线!🚀

结论

这标志着博客的结束。今天,我们深入探讨了许多AWS服务:S3、IAM、ECR、Lambda函数、ECS、Fargate和API Gateway。我们从创建S3存储桶开始项目,最终在ECS集群中部署了我们的应用程序。

在整个指南中,我们涵盖了容器化Next.js应用、将其推送到ECR、配置ECS任务定义以及通过AWS控制台进行部署。这种设置允许自动扩展、轻松更新和安全API访问——这些都是云原生部署的关键优势。

潜在的生产配置可能包括以下更改:

  • 实施更严格的IAM权限,改进对S3存储桶公共访问的控制(使用CloudFront、预签名URL或后端代理,而不是使sample-thumbnails-bucket公开)
  • 添加错误处理和分页(特别是对于DynamoDB查询)
  • 为ECS使用安全的VPC/网络配置(如使用应用程序负载均衡器和私有子网而不是直接公共IP)
  • 通过将元数据获取Lambda中的DynamoDB.scan操作替换为DynamoDB.query来解决扩展问题
  • 在Next.js代码中使用环境变量而不是硬编码的API网关URL
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计