使用 AWS Lambda 的面向服务的架构:分步教程

已发表: 2022-03-11

在构建 Web 应用程序时,有很多选择可以帮助或阻碍您的应用程序,一旦您承诺它们。 语言、框架、托管和数据库等选择至关重要。

一个这样的选择是是使用面向服务的体系结构 (SOA) 创建基于服务的应用程序,还是使用传统的单体应用程序。 这是影响初创公司、扩大规模和企业公司等的常见架构决策。

面向服务的架构被谷歌、Facebook、Twitter、Instagram、优步等一大批知名独角兽和顶尖科技公司采用。 看起来,这种架构模式适用于大公司,但它对你有用吗?

使用 AWS Lambda 的面向服务的架构:分步教程

使用 AWS Lambda 的面向服务的架构:分步教程
鸣叫

在本文中,我们将介绍面向服务的架构这一主题,以及如何利用 AWS Lambda 与 Python 轻松构建可扩展、经济高效的服务。 为了演示这些想法,我们将使用 Python、AWS Lambda、Amazon S3 和其他一些相关工具和服务构建一个简单的图像上传和调整大小服务。

什么是面向服务的架构?

面向服务的体系结构 (SOA) 并不是什么新鲜事物,它起源于几十年前。 近年来,由于为面向 Web 的应用程序提供了许多好处,它作为一种模式越来越受欢迎。

SOA 本质上是将一个大型应用程序抽象为许多相互通信的小型应用程序。 这遵循了软件工程的几个最佳实践,例如解耦、关注点分离和单一职责架构。

SOA 的实现在粒度方面有所不同:从覆盖大范围功能的极少数服务到所谓的“微服务”架构中的数十或数百个小型应用程序。 无论粒度级别如何,SOA 的从业者普遍认为它绝不是免费的午餐。 与软件工程中的许多良好实践一样,这是一项需要额外规划、开发和测试的投资。

什么是 AWS Lambda?

AWS Lambda 是 Amazon Web Services 平台提供的一项服务。 AWS Lambda 允许您上传将在 Amazon 管理的按需容器上运行的代码。 AWS Lambda 将管理服务器的预置和管理以运行代码,因此用户只需要一组运行的打包代码和一些配置选项来定义服务器运行的上下文。 这些托管应用程序称为 Lambda 函数。

AWS Lambda 有两种主要的操作模式:

异步/事件驱动:

Lambda 函数可以在异步模式下运行以响应事件。 任何事件源,例如 S3、SNS 等都不会阻塞,Lambda 函数可以通过多种方式利用这一点,例如为某些事件链建立处理管道。 有许多信息源,根据源,事件将从事件源推送到 Lambda 函数,或由 AWS Lambda 轮询事件。

同步/请求->响应:

对于需要同步返回响应的应用程序,Lambda 可以在同步模式下运行。 通常,这与名为 API Gateway 的服务结合使用,以将来自 AWS Lambda 的 HTTP 响应返回给最终用户,但也可以通过直接调用 AWS Lambda 来同步调用 Lambda 函数。

AWS Lambda 函数作为 zip 文件上传,其中包含处理程序代码以及处理程序操作所需的任何依赖项。 上传后,AWS Lambda 将在需要时执行此代码,并在需要时将服务器数量从零扩展到数千,而无需消费者进行任何额外干预。

Lambda 函数作为 SOA 的演进

基本 SOA 是一种将您的代码库构建成小型应用程序的方法,以便以本文前面​​描述的方式使应用程序受益。 由此,这些应用程序之间的通信方法成为焦点。 事件驱动的 SOA(又名 SOA 2.0)不仅允许 SOA 1.0 的传统直接服务到服务通信,而且还允许在整个架构中传播事件以传达更改。

事件驱动架构是一种自然促进松耦合和可组合性的模式。 通过创建和响应事件,可以临时添加服务以向现有事件添加新功能,并且可以组合多个事件以提供更丰富的功能。

AWS Lambda 可用作轻松构建 SOA 2.0 应用程序的平台。 有很多方法可以触发 Lambda 函数; 从使用 Amazon SNS 的传统消息队列方法,到由上传到 Amazon S3 的文件或使用 Amazon SES 发送的电子邮件创建的事件。

实现一个简单的图片上传服务

我们将构建一个简单的应用程序来使用 AWS 堆栈上传和检索图像。 这个示例项目将包含两个 lambda 函数:一个在 request->response 模式下运行,用于为我们的简单 Web 前端提供服务,另一个将检测上传的图像并调整它们的大小。

第一个 lambda 函数将异步运行,以响应 S3 存储桶上触发的文件上传事件,该存储桶将容纳上传的图像。 它将获取提供的图像并调整其大小以适应 400x400 图像。

另一个 lambda 函数将为 HTML 页面提供服务,为用户提供查看由我们的其他 Lambda 函数调整大小的图像的功能以及用于上传图像的界面。

初始 AWS 配置

在开始之前,我们需要配置一些必要的 AWS 服务,例如 IAM 和 S3。 这些将使用基于 Web 的 AWS 控制台进行配置。 但是,大部分配置也可以通过使用 AWS 命令​​行实用程序来实现,我们稍后将使用该实用程序。

创建 S3 存储桶

S3(或简单存储服务)是一种 Amazon 对象存储服务,可为任何数据提供可靠且经济高效的存储。 我们将使用 S3 存储将要上传的图像,以及我们处理的图像的调整大小版本。

S3 服务可在 AWS 控制台的“存储和内容交付”子部分下的“服务”下拉菜单中找到。 创建存储桶时,系统会提示您输入存储桶名称以及选择区域。 选择靠近用户的区域将使 S3 能够针对延迟和成本以及一些监管因素进行优化。 对于此示例,我们将选择“美国标准”区域。 该区域稍后将用于托管 AWS Lambda 函数。

值得注意的是,S3 存储桶名称必须是唯一的,因此如果选择的名称被采用,您将需要选择一个新的、唯一的名称。

对于这个示例项目,我们将创建两个名为“test-upload”和“test-resized”的存储桶。 “test-upload”存储桶将用于上传图片并在处理和调整大小之前存储上传的图片。 调整大小后,图像将保存到“test-resized”存储桶中,并删除原始上传的图像。

S3 上传权限

默认情况下,S3 权限是限制性的,不允许外部用户甚至非管理用户读取、写入、更新或删除存储桶上的任何权限或对象。 为了改变这一点,我们需要以具有管理 AWS 存储桶权限的用户身份登录。

假设我们在 AWS 控制台上,我们可以通过按名称选择存储桶、单击屏幕右上角的“属性”按钮并打开折叠的“权限”部分来查看上传存储桶的权限。

为了允许匿名用户上传到此存储桶,我们需要编辑存储桶策略以允许允许上传的特定权限。 这是通过基于 JSON 的配置策略完成的。 这类 JSON 策略与 IAM 服务一起在整个 AWS 中广泛使用。 单击“编辑存储桶策略”按钮后,只需粘贴以下文本并单击“保存”以允许上传公共图像:

 { "Version": "2008-10-17", "Id": "Policy1346097257207", "Statement": [ { "Sid": "Allow anonymous upload to /", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::test-upload/*" } ] }

完成此操作后,我们可以通过尝试将图像上传到存储桶来验证存储桶策略是否正确。 以下 cURL 命令可以解决问题:

 curl https://test-upload.s3.amazonaws.com -F 'key=test.jpeg' -F '[email protected]'

如果返回 200 范围的响应,我们就知道上传桶的配置已经成功应用。 我们的 S3 存储桶现在应该(大部分)配置好。 我们稍后将在控制台中返回此服务,以便将我们的图像上传事件连接到我们的调整大小函数的调用。

Lambda 的 IAM 权限

Lambda 角色都在权限上下文中运行,在本例中是由 IAM 服务定义的“角色”。 此角色定义 Lambda 函数在其调用期间拥有的所有权限。 出于本示例项目的目的,我们将创建一个将在两个 Lambda 函数之间使用的通用角色。 但是,在生产场景中,建议在权限定义中使用更精细的粒度,以确保任何安全漏洞都被隔离到仅定义的权限上下文中。

IAM 服务可以在“服务”下拉菜单的“安全和身份”子部分中找到。 IAM 服务是一个非常强大的跨 AWS 服务访问管理工具,如果您不熟悉类似的工具,提供的界面一开始可能会有点不知所措。

进入 IAM 仪表板页面后,可以在页面左侧找到“角色”子部分。 从这里我们可以使用“创建新角色”按钮来调出一个多步骤向导来定义角色的权限。 让我们使用“lambda_role”作为我们的通用权限的名称。 从名称定义页面继续后,您将看到选择角色类型的选项。 由于我们只需要 S3 访问权限,因此单击“AWS 服务角色”并在选择框中选择“AWS Lambda”。 您将看到可以附加到此角色的策略页面。 选择“AmazonS3FullAccess”策略并继续下一步以确认要创建的角色。

请务必记下所创建角色的名称和 ARN(Amazon 资源名称)。 这将在创建新的 Lambda 函数时使用,以识别要用于函数调用的角色。

注意: AWS Lambda 将自动记录来自 AWS Cloudwatch(一种日志服务)中函数调用的所有输出。 如果需要此功能(推荐用于生产环境),则必须将写入 Cloudwatch 日志流的权限添加到此角色的策略中。

代码!

概述

现在我们准备开始编码。 我们将假设此时您已经设置了“awscli”命令。 如果您还没有,您可以按照 https://aws.amazon.com/cli/ 上的说明在您的计算机上设置 awscli。

注意:为了便于屏幕查看,这些示例中使用的代码被缩短了。 如需更完整的版本,请访问位于 https://github.com/gxx/aws-lambda-python/ 的存储库。

首先,让我们为我们的项目建立一个骨架结构。

 aws-lambda-python/ - image_list/ - handler.py - list.html - Makefile - requirements.txt - image_resize/ - handler.py - resize.py - Makefile - requirements.txt - .pydistutils.cfg

我们的结构中有两个子目录,一个用于每个 lambda 函数。 在每个文件中,我们都有通用的 handler.py、Makefile 和 requirements.txt 文件。 handler.py 文件将包含调用每个 lambda 函数的方法,并且可以被认为是函数的入口点。 requirements.txt 文件将包含我们的依赖项列表,因此我们可以轻松指定和更新需求。 最后,我们将使用 Makefile 命令提供与 awscli 交互的简单机制。 这将使创建和更新我们的 lambda 函数的过程变得更加容易。

您会注意到项目目录根目录中的 .pydistutils.cfg 文件。 如果您在 Homebrew 下使用 Python,则需要此文件。 由于部署 Lambda 函数的方法(在下一节中介绍),需要此文件。 有关更多详细信息,请参阅存储库。

调整图像大小 Lambda 函数

代码

从 resize_image 函数开始,我们将冻结 Wand 依赖项,即我们的图像处理库,将Wand==0.4.2保存到 requirements.txt。 这将是我们的 image_resize lambda 函数的唯一依赖项。 resize.py 中的 resize_image 函数将需要处理来自 Wand 库的图像资源,并根据指定的宽度和高度参数调整其大小。 为了保留正在调整大小的图像,我们将使用“最佳匹配”调整大小算法,该算法将保持原始图像的图像比例,同时减小图像大小以适应指定的宽度和高度。

 def resize_image(image, resize_width, resize_height): ... original_ratio = image.width / float(image.height) resize_ratio = resize_width / float(resize_height) # We stick to the original ratio here, regardless of what the resize ratio is if original_ratio > resize_ratio: # If width is larger, we base the resize height as a function of the ratio of the width resize_height = int(round(resize_width / original_ratio)) else: # Otherwise, we base the width as a function of the ratio of the height resize_width = int(round(resize_height * original_ratio)) if ((image.width - resize_width) + (image.height - resize_height)) < 0: filter_name = 'mitchell' else: filter_name = 'lanczos2' image.resize(width=resize_width, height=resize_height, filter=filter_name, blur=1) return image

除此之外,需要一个处理函数来接受由 S3 上传的图像生成的事件,将其交给resize_image函数,并保存生成的调整大小的图像。

 from __future__ import print_function import boto3 from wand.image import Image from resize import resize_image def handle_resize(event, context): # Obtain the bucket name and key for the event bucket_name = event['Records'][0]['s3']['bucket']['name'] key_path = event['Records'][0]['s3']['object']['key'] response = boto3.resource('s3').Object(bucket_name, key_path).get() # Retrieve the S3 Object # Perform the resize operation with Image(blob=response['Body'].read()) as image: resized_data = resize_image(image, 400, 400).make_blob() # And finally, upload to the resize bucket the new image s3_connection.Object('test-resized', key_path).put(ACL='public-read', Body=resized_data) # Finally remove, as the bucket is public and we don't want just anyone dumping the list of our files! s3_object.delete()

部署

代码完成后,需要将其作为新的 Lambda 函数上传到 Amazon Lambda。 这就是添加到目录结构中的 Makefile 发挥作用的地方。 此 Makefile 将用于部署我们正在创建的 Lambda 函数定义。

 ROLE_ARN = arn:aws:iam::601885388019:role/lambda_role FUNCTION_NAME = ResizeImage REGION = us-west-1 TIMEOUT = 15 MEMORY_SIZE = 512 ZIPFILE_NAME = image_resize.zip HANDLER = handler.handle_resize clean_pyc : find . | grep .pyc$ | xargs rm install_deps : pip install -r requirements.txt -t . build : install_deps clean_pyc zip $(ZIPFILE_NAME) -r * create : build aws lambda create-function --region $(REGION) \ --function-name $(FUNCTION_NAME) \ --zip-file fileb://$(ZIPFILE_NAME) \ --role $(ROLE_ARN) \ --handler $(HANDLER) \ --runtime python2.7 \ --timeout $(TIMEOUT) \ --memory-size $(MEMORY_SIZE) update : build aws lambda update-function-code --region $(REGION) \ --function-name $(FUNCTION_NAME) \ --zip-file fileb://$(ZIPFILE_NAME) \ --publish

这个 Makefile 的主要功能是“创建”和“更新”。 这些函数首先打包当前目录,该目录代表运行 Lambda 函数所需的所有代码。 接下来,将requirements.txt文件中指定的任何依赖项安装到当前要打包的子目录中。 打包步骤压缩目录的内容,以便稍后使用“awscli”命令上传。 我们的 Makefile 可以适用于其他 Lambda 函数定义。

在实用程序 Makefile 中,我们定义了一些创建/更新图像所需的变量。 根据需要配置这些,以使 Makefile 命令为您正常工作。

  • ROLE_ARN :这是标识我们在其下运行 Lambda 函数的角色的 Amazon 资源名称。
  • FUNCTION_NAME :我们正在创建/更新的 Lambda 函数的名称。
  • REGION :将在其下创建/更新 Lambda 函数的区域。
  • TIMEOUT :在 Lambda 调用被终止之前的超时时间(以秒为单位)。
  • MEMORY_SIZE :Lambda 函数在调用时将有权访问的内存大小(以 MB 为单位)。
  • ZIPFILE_NAME :包含 Lambda 函数代码和依赖项的压缩包的名称。
  • HANDLER :处理函数的绝对导入路径,以点表示法。

配置完成后,运行make create命令将生成类似于以下输出的内容:

 $ make create pip install -r requirements.txt -t . ... find . | grep .pyc| xargs rm zip image_resize.zip -r * ... aws lambda create-function --region ap-northeast-1 \ --function-name ResizeImage2 \ --zip-file fileb://image_resize.zip \ --role arn:aws:iam::11111111111:role/lambda_role \ --handler handler.handle_resize \ --runtime python2.7 \ --timeout 15 \ --memory-size 512 { "CodeSha256": "doB1hsujmZnxZHidnLKP3XG2ifHM3jteLEBvsK1G2nasKSo=", "FunctionName": "ResizeImage", "CodeSize": 155578, "MemorySize": 512, "FunctionArn": "arn:aws:lambda:us-west-1:11111111111:function:ResizeImage", "Version": "$LATEST", "Role": "arn:aws:iam::11111111111:role/lambda_role", "Timeout": 15, "LastModified": "2016-01-10T11:11:11.000+0000", "Handler": "handler.handle_resize", "Runtime": "python2.7", "Description": "" }

测试

执行创建命令后,我们的图像的大小调整功能就可以使用了,但是它还没有连接到 S3 存储桶以接收事件。 我们仍然可以通过 AWS 控制台测试该函数,以断言该函数正确处理 S3 文件上传事件。 在 AWS Lambda 控制面板上,可以在“服务”下拉列表的“计算”子部分下找到,选择我们创建的函数的名称。 此 Lambda 函数详细信息页面提供了一个标记为“操作”的下拉菜单,其中包含一个标记为“配置测试事件”的选项。

单击此按钮将打开一个模式,允许您指定测试事件和一些示例模板。 选择“S3 Put”示例,并将任何提及的存储桶名称替换为已设置的存储桶名称。 配置完成后,后续使用 Lambda 函数页面上的“测试”按钮将调用 Lambda 函数,就好像之前配置的事件确实发生了一样。

为了监控任何错误堆栈跟踪或日志消息,您可以在 Cloudwatch 中查看日志流。 将在创建 Lambda 函数的同时创建一个新的日志组。 这些日志流很有用,可以通过管道传输到其他服务中。

连接到 S3 存储桶事件

返回 S3 仪表板,展开位于“属性”菜单中的折叠“事件”部分以显示“事件通知”表单。 我们必须填写的字段是“事件”和“发送至”输入。 从“事件”中,选择“对象已创建(全部)”事件。 这将允许拦截在上传存储桶上创建对象的所有事件。 对于“Sent To”输入,选择“Lambda function”单选按钮。 将出现一个新部分,其中包含一个下拉菜单,其中包含我们已配置为选项的“ResizeImage”lambda 函数。 单击“保存”后,任何“已创建对象”事件现在都将作为输入路由到“ResizeImage”Lambda 函数的调用。

我们现在拥有应用程序的核心功能。 让我们运行另一个 cURL 测试以确保一切都按预期工作。 使用 cURL 将图片上传到 S3 存储桶中,并手动检查该图片是否已上传到 resize 存储桶中。

 curl https://test-upload.s3.amazonaws.com -F 'key=test.jpeg' -F '[email protected]'

执行此命令后,应在 50-1000 毫秒后在“test-resized”存储桶中创建调整大小的图像,具体取决于 Lambda 函数是否已经“预热”。

列出图像 Lambda 函数

代码

ListImage Lambda 函数将检索调整大小的图像列表并将它们显示在 HTML 页面上供用户使用。 该 HTML 页面还为用户提供上传自己的图像的功能。 Jinja2 在函数中用于从模板定义呈现 HTML。 和以前一样,这些要求在requirements.txt文件中指定。

 from __future__ import print_function import os import boto3 from jinja2 import Environment from jinja2 import FileSystemLoader def _render_template(image_urls): env = Environment(loader=FileSystemLoader(os.path.abspath(os.path.dirname(__file__)))) template = env.get_template('list.html') rendered_template = template.render(image_urls=image_urls) return rendered_template def handle_list_image(event, context): bucket = boto3.resource('s3').Bucket('test-resized') image_summaries = sorted((image_summary for image_summary in bucket.objects.all()), key=lambda o: o.last_modified) image_urls = [] for summary in image_summaries: image_urls.append( boto3.client('s3').generate_presigned_url( 'get_object', Params={ 'Bucket': summary.bucket_name, 'Key': summary.key } ) ) return {'htmlContent': _render_template(image_urls)}
 <html> <head> <title>List Images</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> <script> var uploadImage = (function () { var inProgress = false; return function () { if (inProgress) { return; } inProgress = true; var formData = new FormData(); var fileData = $('#image-file').prop('files')[0]; formData.append('key', parseInt(Math.random() * 1000000)); formData.append('acl', 'public-read'); formData.append('file', fileData); $.ajax({ url: 'https://test-upload.s3.amazonaws.com/', type: 'POST', data: formData, processData: false, contentType: false, success: function (data) { window.location.reload(); } }); } })(); </script> <style type="text/css"> .image__container { float: left; width: 30%; margin-left: 2.5%; margin-right: 2.5%; max-width: 400px; } </style> </head> <body> <nav> <input type="file" onchange="uploadImage()" value="Upload Image" /> </nav> <section> {% for image_url in image_urls %} <div class="image__container"> <img src="{{ image_url }}" /> </div> {% endfor %} </section> </body> </html>

再一次,我们可以改变之前的 Makefile 并使用 create 命令来部署我们的 lambda 函数。

API 网关

ImageList Lambda 函数已完成,但无法提供给任何用户。 这是因为 Lambda 函数只能被调用以响应来自另一个服务的事件,或者以编程方式。 这就是 Amazon AWS API Gateway 服务应运而生的地方。 API 网关可以在“应用程序服务”子部分下找到。

API Gateway 是一种将端点建模为一组资源和方法的方法,本质上是一个 REST 接口。 除了提供验证和转换请求的工具外,API Gateway 还执行其他功能,例如提供限制/速率限制请求。

在 API Gateway 仪表板中,创建一个新的 API 来为 ListImage 函数提供服务。 名称和描述可以根据您的喜好设置。 创建后,单击新 API 的名称以访问 API 详细信息。 为根 URL“/”创建一个新资源。 此 URL 将用于提供 HTML 页面。

在查看根资源页面的详细信息时,添加一个 GET 方法。 将“集成类型”设置为“Lambda 函数”,将“Lambda 区域”设置为“us-west-1”或您选择的区域,然后键入 ListImage Lambda 函数的名称。

在开始将响应映射到 HTML 输出之前,我们需要定义一个“模型”,除了将响应映射到内容类型之外,它还将定义来自服务器的响应的模式。 选择 API 的“模型”部分,然后单击“创建”以创建新模型。 将模型命名为“HTML”,内容类型为“text/html”,并按如下方式定义模式:

 { "$schema": "http://json-schema.org/draft-04/schema#", "title" : "HTML", "type" : "object" }

返回 API 仪表板,选择我们创建的资源并导航到“集成响应”部分。 本节定义在接收到来自 Lambda 函数的响应之后,在将响应传递到最后一步之前要处理的任何转换。

打开“映射模板”部分并添加一个新的“内容类型”“text/html”。 同时删除旧的“内容类型”。 在右侧,将下拉菜单从“Output Passthrough”更改为“Mapping template”。 这将允许我们更改 API Gateway 接受的原始 JSON,并使用我们返回数据的“htmlContent”属性中的 HTML 内容。 对于映射模板,指定“$input.htmlContent”作为模板。 最后,通过从“200 的响应模型”中删除“application/json”并添加“text/html”来更改“方法响应”部分。

返回到 API 的仪表板,页面左上角有一个标记为“部署 API”的按钮。 单击此按钮可更新或创建具有指定资源、方法、模型和映射的 API。 完成此操作后,将显示所选部署阶段的 URL(默认为阶段)。 最后,示例完成! 您可以上传一些文件来测试和查看调整大小的图像。

包起来

AWS 是一项大型服务,不会很快消失。 尽管供应商锁定始终是需要小心的事情,但 AWS Lambda 提供了一项相对精简的服务,具有一组丰富的辅助配置选项。 利用 AWS 提供的服务来实施易于扩展和维护的应用程序将提供使用 AWS 平台的最大好处。 AWS Lambda 提供优雅、可扩展且经济高效的解决方案,由大量消费者使用的企业级平台提供支持。 我相信“无服务器”应用程序是未来的方式。 在下面的评论中让我们知道您的想法。