亚马逊AWS官方博客

利用无服务器技术快速构建全球分布式应用

1. 本文概述

本文将介绍如何如何快速将容器化应用改造为无服务器应用,并快速发布到全球各个区域,利用亚马逊云科技的全球基础设施及快速网络实现最佳的终端客户访问体验。

2. 场景描述

越来越多的客户考虑将业务部署到全球多个区域,以实现客户的就近访问,并达到最佳客户访问体验。亚马逊云科技提供了非常多实现就近访问和分布式部署的方案,例如 Route53 可以实现域名基于地理位置或者延迟的解析路由,CloudFront 可以实现静态文件的全球加速访问。亚马逊云科技大部分数据库也提供了全球部署的架构,例如 Amazon Aurora Global Database、DynamoDB Global Tables、Elasticache Global DataStore 等,这些都可以方便地帮助客户实现全球化分布式的架构。

但是,对于一些客户动态内容发布的用户,尤其是使用了 React 框架构建了动态内容渲染的 HTTP 应用,通常需要使用 EC2 或者容器将应用分布式部署,而这些 EC2 或者容器的基础设施的运维和扩展都会增加客户的运维成本。这时可以考虑使用本方案,将应用改造为无服务器应用,并利用 Route53 和 CloudFront 实现全球加速访问。

3. 解决方案

3.1 方案架构

架构说明:

  • 将容器化应用改造为无服务器应用,部署到多个区域,并使用 Lambda URL 暴露,代码由 ECR 托管
  • 将 Lambda URL 作为 CloudFront 的回源地址,并处理请求安全问题
  • 将用户的请求使用 Route 53 和 CloudFront 就近回源请求到离用户最近的 Lambda URL

3.2 实现原理和过程

为了实现上述效果,我们需要解决以下问题:

  • 如何将容器化应用改造为无服务器应用?

由于容器化应用通常接收 Http 或者 API 请求,但是 Lambda 是基于 Event 请求触发,这里可以使用 AWS Lambda Web Adapter[https://github.com/aws-samples/aws-lambda-web-adapter]方案将 Http 请求转换为 Event 请求,然后使用 Lambda 处理请求。AWS Lambda Web Adapter 支持大部分的主流框架,包括 Express.js、Next.js、Flask、SpringBoot、 ASP.NET 等,可以通过一行代码轻松改造。

  • Lambda URL 如果公开访问会有安全风险,如何处理?

这里使用 IAM 认证和授权,但这里 CloudFront 不能通过直接配置 OAC 处理权限/签名访问问题,因为 OAC 目前只支持 Get 的请求签名,如果有 post 访问,需要额外处理,因此这里选择用 Lambda edge 做手动签名处理。

  • 如何让 CloudFront 回源到离用户最近的 Lambda URL?

这里不能直接使用 Route53 的延迟路由或区域访问路由(无法配置),而且本身 Lambda URL 也没有直接公开,无法通过 Route53 直接访问,这里我们使用 Route53 配合 CloudFront Lambda@Edge 处理这部分逻辑。

3.3 实现代码(代码节选)

为了方便多区域部署和代码更新,我们这里使用 AWS CDK 实现整个方案的部署,AWS 云开发工具包(AWS CDK)是一个开源软件开发框架,可让您使用现代编程语言以代码形式定义云基础设施,并通过 AWS CloudFormation 部署该基础设施。

这里我们定义了三个 Stack,分别是:

  • LambdaApplicationStack:部署 Lambda 函数,并使用 Lambda Web Adapter 方案将容器化应用改造为无服务器应用,并将应用使用 Lambda URL 暴露。
  • CloudfrontStack:部署 CloudFront,并使用 Lambda@Edge 处理回源逻辑,并使用 Lambda URL 进行签名认证。
  • Route53Stack:部署 Route53 记录,并使用 Route53 判断延迟最低的回源路径。

3.3.1 容器应用处理

我们这里使用 aws-lambda-web-adapter 将前端的容器应用直接转换为 Lambda Serverless 应用。AWS Lambda Web 适配器允许开发者使用熟悉的框架构建 Web 应用(HTTP API)然后在 AWS Lambda 上运行。实际转换过程非常简单,只需要在 Dockerfile 中增加一行代码。

FROM public.ecr.aws/docker/library/node:20-slim
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=7000
WORKDIR "/var/task"
ADD src/package.json /var/task/package.json
ADD src/package-lock.json /var/task/package-lock.json
RUN npm install --omit=dev
ADD src/ /var/task
CMD ["node", "index.js"]

3.3.2 LambdaApplicationStack 说明

LambdaStack 的主要作用是改造容器化应用为无服务器应用,并且部署到多个区域,使用 Lambda URL 暴露。

主要功能代码如下:

# 构建并推送镜像到 ECR
class LambdaApplicationStack(Stack):
    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
        # 构建并推送镜像到 ECR
        ecr_image = aws_lambda.EcrImageCode.from_asset_image(
            directory=os.path.join(os.getcwd(), "app_docker")
        )

        # 创建 Lambda 函数
        myfunc = aws_lambda.Function(
            self, "lambdaContainerFunction",
            description="Lambda Container Function",
            code=ecr_image,
            handler=aws_lambda.Handler.FROM_IMAGE,
            runtime=aws_lambda.Runtime.FROM_IMAGE,
            function_name=f"LambdaApplicationStack-function-{current_region}",
            memory_size=128,
            reserved_concurrent_executions=10,
            timeout=Duration.seconds(120)
        # 添加函数 URL
        function_url = myfunc.add_function_url(
            auth_type=aws_lambda.FunctionUrlAuthType.AWS_IAM,
            invoke_mode=aws_lambda.InvokeMode.BUFFERED #如果需要流式输出需要修改为RESPONSE_STREAM
        )
        # 输出函数 URL
        CfnOutput(self, "LambdaApplication-function-url", value=function_url.url)

3.3.3 CloudFrontStack 说明

CloudFrontStack 的主要作用是部署 Cloudfront,使用 Lambda@Edge 处理请求,并使用 Lambda URL 进行签名认证。

主要功能代码如下:

class CloudfrontStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, LambdaApplicationStack: LambdaApplicationStack, **kwargs) -> None:
        ## 创建Lambda@Edge 函数
        edgelambda = aws_lambda.Function(self, "edgelambda",
            code=aws_lambda.Code.from_asset("cloudfront_function/edge_lambda"),
            handler="auth_lambda_handler.lambda_handler",
            runtime=aws_lambda.Runtime.PYTHON_3_11,
            role=edge_lambda_role,
            timeout=Duration.seconds(10)
        )
        CloudfrontStack_distribution = cloudfront.Distribution(self, "CloudfrontStack-distribution",
            default_behavior=cloudfront.BehaviorOptions(
                compress=True,
                cache_policy=cloudfront.CachePolicy.CACHING_DISABLED,
                allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL,
                origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER,
                origin=origins.HttpOrigin(domain_name=custom_origin,custom_headers={"TARGET_ORIGIN":self.node.try_get_context('custom_origin')}),# 这里创建一个自定义的Header 用于将回源地址传递到Lambda@Edge代码中,Lambda@Edge代码具体参考repo中的相关代码.
                edge_lambdas=[cloudfront.EdgeLambda(
                    function_version=edgelambda.current_version,
                    event_type=cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
                    include_body=True
                )]
            )
        ) 

3.3.4 Route53Stack 说明

Route53Stack 的主要作用是创建一个 Route53 记录,用于判断用户请求到哪个区域延迟最低。

主要功能代码如下:

class Route53Stack(Stack):
     # 从 context 获取 custom_origin 这里需要客户提供一个域名用于创建基于延迟的Route53记录
    custom_origin = self.node.try_get_context('custom_origin')
    if not custom_origin:
        raise ValueError("No custom_origin specified in cdk.context.json")
        # 为每个区域的 Lambda URL 创建记录
    for region, stack in LambdaApplicationStack_stacks.items():
        # 创建延迟路由记录
        route53.CnameRecord(
            self, f"LatencyRecord-{region}",
            zone=hosted_zone,
            record_name=custom_origin,
            domain_name=stack.function_url.replace("https://", ""),  # 使用 Lambda URL
            region=region  # 设置区域即可启用延迟路由
        )
        # 输出托管区 ID
        CfnOutput(self, "HostedZoneId", value=hosted_zone.hosted_zone_id) 

4. 解决方案部署

4.1 部署过程

为了方便部署,我们这里将整个解决方案打包成并使用 CDK 部署,使用 cdk deploy 命令就可以将应用部署到多个区域。

在部署前,需要编辑 cdk.context.json 文件:

{
  "custom_origin": "latency.example.com", # 用于创建基于延迟的Route53记录
  "LambdaApplicationStack_regions": [
    "us-west-2", # 应用需要部署的区域
    "us-east-1",
    "ap-northeast-1"
  ],
  "hosted_zone_id": "", # 如果需要在已存在的hosted zone 中创建记录,需要提供hosted zone id,否则留空.
  "edgelambda_region": "us-east-1", # Lambda@Edge 部署的区域
  "acknowledged-issue-numbers": [
    32775, # 忽略的issue 编号
    32775
  ]
}

接下来,将需要部署的应用代码和 Dockerfile 文件复制到 app_docker 目录下,然后按照 Readme 说明执行 cdk deploy 命令就可以将应用部署到多个区域。最终会输出一个 CloudFront URL,作为最终的访问地址。

部署后会创建以下资源,包括:

  • ECR 镜像,用于存放容器化应用的镜像
  • Lambda 函数,用于响应 Http 请求
  • Cloudfront 分配,用于回源到主域名,由 CloudFront Lambda@Edge 基于延迟路由回源到到最近的 Lambda URL
  • Route53 记录,用于判断延迟最低的回源路径

部署完成后可以通过最终的 CloudFront URL 地址访问(以下示例为一个 AI 生成应用,并使用了流式响应)。

4.2 部署效果

通过将访问前端分布式的部署到各个区域,可以看到各个区域的访问延迟显著降低,从之前平均 2000+ms 下降到 200+ms,这其中一部分得益于就近部署客户访问的路径更短,另外也得益于 CloudFront 就近回源使得用户可以最快地访问到应用。

此外我们还观察到,除了明显的延迟变化,整个访问的抖动情况也得到了显著改善。这是由于 Lambda 应用的弹性更快,资源的隔离性相较于容器也更好,用户体验会更好。

5. 总结

通过以上方案,我们可以将容器化应用改造为无服务器应用,并利用 Route53 和 CloudFront 实现全球加速访问,在实际客户实践中,对比使用传统的 EC2 或 EKS 部署,可以显著降低成本,并且可以通过 CDK 实现快速部署和更新。

参考文档

AWS CDK 文档:https://aws.amazon.com/cdk
AWS Lambda Web Adapter 说明:https://github.com/aws-samples/aws-lambda-web-adapter
代码参考:https://github.com/brilliantwf/Serverless_App_deploy

本篇作者

王非

亚马逊云科技解决方案架构师,负责基于亚马逊云科技云计算方案的架构咨询和设计实现,同时致力于物联网服务的应用以及推广和推进企业服务迁移上云进程。