使用 AWS Lambda 函数构建您的第一个无服务器服务

Avatar of Adam Rackis
Adam Rackis

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

许多开发人员至少对 AWS Lambda 函数有所了解。它们设置起来相当简单,但广阔的 AWS 环境可能难以看到全局。由于有很多不同的部分,这可能令人生畏,并且令人沮丧地难以看到它们如何无缝地融入到普通的 Web 应用程序中。

Serverless 框架在这里提供了巨大的帮助。它简化了 Lambda 函数的创建、部署以及最重要的,将其集成到 Web 应用程序中。需要澄清的是,它不仅限于此,但这些是我将重点关注的部分。希望这篇文章能激发您的兴趣,并鼓励您查看 Serverless 支持的许多其他内容。如果您完全不熟悉 Lambda,您可能需要先查看 AWS 入门指南

我无法比 快速入门指南 更好地介绍初始安装和设置,所以从那里开始,让您快速上手。假设您已经拥有 AWS 帐户,您可能在 5-10 分钟内就能运行起来;如果您没有,该指南也涵盖了这一点。

您的第一个 Serverless 服务

在我们开始处理文件上传和 S3 存储桶等酷炫的东西之前,让我们创建一个基本的 Lambda 函数,将其连接到 HTTP 端点,并从现有的 Web 应用程序中调用它。该 Lambda 不会执行任何有用的或有趣的操作,但这将为我们提供一个很好的机会来了解使用 Serverless 的便利性。

首先,让我们创建我们的服务。打开任何新的或您可能拥有的现有 Web 应用程序(create-react-app 是快速启动新应用程序的好方法)并找到创建我们的服务的位置。对我来说,这是我的 lambda 文件夹。无论您选择哪个目录,从终端中 cd 到该目录并运行以下命令

sls create -t aws-nodejs --path hello-world

这将创建一个名为 hello-world 的新目录。让我们打开它并查看里面有什么。

如果您查看 handler.js,您应该会看到一个返回消息的异步函数。我们现在可以在终端中运行 sls deploy 来部署该 Lambda 函数,然后可以调用它。但在我们这样做之前,让我们使其可通过 Web 调用。

手动使用 AWS 时,我们通常需要进入 AWS API Gateway,创建端点,然后创建阶段,并告诉它代理到我们的 Lambda。使用 Serverless,我们只需要一些配置。

仍然在 hello-world 目录中?打开在其中创建的 serverless.yaml 文件。

配置文件实际上带有最常见设置的样板代码。让我们取消对 http 条目的注释,并添加更合理的路径。类似于以下代码

functions:
  hello:
    handler: handler.hello
#   The following are a few example events you can configure
#   NOTE: Please make sure to change your handler code to work with those events
#   Check the event documentation for details
    events:
      - http:
        path: msg
        method: get

就这样。Serverless 完成了上面描述的所有繁琐工作。

CORS 配置

理想情况下,我们希望使用前端 JavaScript 代码中的 Fetch API 来调用它,但这不幸地意味着我们需要配置 CORS。本节将引导您完成这一过程。

在上面的配置下方,添加 cors: true,如下所示

functions:
  hello:
    handler: handler.hello
    events:
      - http:
        path: msg
        method: get
        cors: true

就是这一部分!CORS 现在已在我们的 API 端点上配置,允许跨域通信。

CORS Lambda 调整

虽然我们的 HTTP 端点已配置为 CORS,但我们的 Lambda 需要返回正确的标头。这就是 CORS 的工作原理。让我们通过返回 handler.js 并添加以下函数来自动执行此操作

const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});

在从 Lambda 返回之前,我们将通过该函数发送返回值。以下是包含我们到目前为止所做的所有操作的 handler.js 的完整内容

'use strict';
const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});


module.exports.hello = async event => {
  return CorsResponse("HELLO, WORLD!");
};

让我们运行它。在您的终端中,从 hello-world 文件夹中键入 sls deploy

运行完成后,我们将把 Lambda 函数部署到一个 HTTP 端点,我们可以通过 Fetch 调用它。但是,它在哪里?我们可以打开 AWS 控制台,找到 Serverless 为我们创建的网关 API,然后找到调用 URL。它看起来类似于以下代码。

The AWS console showing the Settings tab which includes Cache Settings. Above that is a blue notice that contains the invoke URL.

幸运的是,有一种更简单的方法,那就是在终端中键入 sls info

就这样,我们可以看到我们的 Lambda 函数在以下路径可用

https://6xpmc3g0ch.execute-api.us-east-1.amazonaws.com/dev/ms

太棒了,现在让我们调用它!

现在让我们打开一个 Web 应用程序,并尝试获取它。以下是 Fetch 的样子

fetch("https://6xpmc3g0ch.execute-api.us-east-1.amazonaws.com/dev/msg")
  .then(resp => resp.json())
  .then(resp => {
    console.log(resp);
  });

我们应该在开发者控制台中看到我们的消息。

Console output showing Hello World.

现在我们已经熟悉了,让我们重复此过程。不过,这次让我们创建一个更有意思、更有用的服务。具体来说,让我们创建典型的“调整图像大小”Lambda,但不是通过新的 S3 存储桶上传触发,而是让用户直接将图像上传到我们的 Lambda。这样就无需在客户端捆绑包中捆绑任何类型的 aws-sdk 资源。

构建一个有用的 Lambda

好的,从头开始!这个特定的 Lambda 将获取一个图像,调整其大小,然后将其上传到 S3 存储桶。首先,让我们创建一个新的服务。我将其称为 cover-art,但它可以是任何其他名称。

sls create -t aws-nodejs --path cover-art

和之前一样,我们将添加一个到 HTTP 端点的路径(在这种情况下,它将是 POST,而不是 GET,因为我们正在发送文件,而不是接收它),并启用 CORS

// Same as before
  events:
    - http:
      path: upload
      method: post
      cors: true

接下来,让我们授予我们的 Lambda 访问我们将在上传过程中使用的任何 S3 存储桶的权限。查看您的 YAML 文件,应该有一个 iamRoleStatements 部分,其中包含已注释掉的样板代码。我们可以通过取消注释它来利用其中的一些代码。以下是我们用于启用所需 S3 存储桶的配置

iamRoleStatements:
 - Effect: "Allow"
   Action:
     - "s3:*"
   Resource: ["arn:aws:s3:::your-bucket-name/*"]

注意结尾处的 /*。我们不会单独列出特定的存储桶名称,而是列出资源的路径;在这种情况下,是 your-bucket-name 中可能存在的任何资源。

由于我们希望直接将文件上传到我们的 Lambda,因此我们需要进行一项额外的调整。具体来说,我们需要将 API 端点配置为接受 multipart/form-data 作为二进制媒体类型。在 YAML 文件中找到 provider 部分

provider:
  name: aws
  runtime: nodejs12.x

…并将其修改为

provider:
  name: aws
  runtime: nodejs12.x
  apiGateway:
    binaryMediaTypes:
      - 'multipart/form-data'

为了保险起见,让我们给我们的函数一个智能的名称。将 handler: handler.hello 替换为 handler: handler.upload,然后将 handler.js 中的 module.exports.hello 更改为 module.exports.upload

现在我们可以编写一些代码了

首先,让我们获取一些辅助程序。

npm i jimp uuid lambda-multipart-parser

等等,什么是 Jimp?它是我用来调整上传图像大小的库。uuid 用于在上传到 S3 之前创建新的、唯一的已调整大小资源文件名。哦,还有 lambda-multipart-parser?它用于解析我们 Lambda 中的文件信息。

接下来,让我们为 S3 上传创建一个方便的辅助程序

const uploadToS3 = (fileName, body) => {
  const s3 = new S3({});
  const  params = { Bucket: "your-bucket-name", Key: `/${fileName}`, Body: body };


  return new Promise(res => {
    s3.upload(params, function(err, data) {
      if (err) {
        return res(CorsResponse({ error: true, message: err }));
      }
      res(CorsResponse({ 
        success: true, 
        url: `https://${params.Bucket}.s3.amazonaws.com/${params.Key}` 
      }));
    });
  });
};

最后,我们将插入一些代码,这些代码将读取上传的文件,使用 Jimp(如果需要)调整它们的大小,并将结果上传到 S3。最终结果如下所示。

'use strict';
const AWS = require("aws-sdk");
const { S3 } = AWS;
const path = require("path");
const Jimp = require("jimp");
const uuid = require("uuid/v4");
const awsMultiPartParser = require("lambda-multipart-parser");


const CorsResponse = obj => ({
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "*"
  },
  body: JSON.stringify(obj)
});


const uploadToS3 = (fileName, body) => {
  const s3 = new S3({});
  var params = { Bucket: "your-bucket-name", Key: `/${fileName}`, Body: body };
  return new Promise(res => {
    s3.upload(params, function(err, data) {
      if (err) {
        return res(CorsResponse({ error: true, message: err }));
      }
      res(CorsResponse({ 
        success: true, 
        url: `https://${params.Bucket}.s3.amazonaws.com/${params.Key}` 
      }));
    });
  });
};


module.exports.upload = async event => {
  const formPayload = await awsMultiPartParser.parse(event);
  const MAX_WIDTH = 50;
  return new Promise(res => {
    Jimp.read(formPayload.files[0].content, function(err, image) {
      if (err || !image) {
        return res(CorsResponse({ error: true, message: err }));
      }
      const newName = `${uuid()}${path.extname(formPayload.files[0].filename)}`;
      if (image.bitmap.width > MAX_WIDTH) {
        image.resize(MAX_WIDTH, Jimp.AUTO);
        image.getBuffer(image.getMIME(), (err, body) => {
          if (err) {
            return res(CorsResponse({ error: true, message: err }));
          }
          return res(uploadToS3(newName, body));
        });
      } else {
        image.getBuffer(image.getMIME(), (err, body) => {
          if (err) {
            return res(CorsResponse({ error: true, message: err }));
          }
          return res(uploadToS3(newName, body));
        });
      }
    });
  });
};

抱歉给您发布了这么多代码,但是,这是一篇关于 Amazon Lambda 和无服务器的文章,我宁愿不详细说明无服务器函数中的繁琐工作。当然,如果您使用的是 Jimp 以外的图像库,您的代码可能看起来完全不同。

让我们通过从我们的客户端上传文件来运行它。我使用的是 react-dropzone 库,所以我的 JSX 如下所示

<Dropzone
  onDrop={files => onDrop(files)}
  multiple={false}
>
  <div>Click or drag to upload a new cover</div>
</Dropzone>

onDrop 函数如下所示

const onDrop = files => {
  let request = new FormData();
  request.append("fileUploaded", files[0]);


  fetch("https://yb1ihnzpy8.execute-api.us-east-1.amazonaws.com/dev/upload", {
    method: "POST",
    mode: "cors",
    body: request
    })
  .then(resp => resp.json())
  .then(res => {
    if (res.error) {
      // handle errors
    } else {
      // success - woo hoo - update state as needed
    }
  });
};

就这样,我们可以上传文件,并看到它出现在我们的 S3 存储桶中!

Screenshot of the AWS interface for buckets showing an uploaded file in a bucket that came from the Lambda function.

可选的绕行:捆绑

我们可以在设置中进行一项可选的增强。目前,当我们部署服务时,Serverless 会将整个服务文件夹压缩并发送到我们的 Lambda。当前内容重达 10MB,因为所有 node_modules 都被拖进了 Lambda。我们可以使用捆绑器来大幅减少这个大小。不仅如此,捆绑器还可以缩短部署时间、减少数据使用量、改善冷启动性能等等。换句话说,这是一个不错的选择。

幸运的是,有一个插件可以轻松地将 webpack 集成到 serverless 构建过程中。让我们用以下命令安装它:

npm i serverless-webpack --save-dev

…并通过我们的 YAML 配置文件添加它。我们可以在最后添加它

// Same as before
plugins:
  - serverless-webpack

当然,我们需要一个 webpack.config.js 文件,所以让我们把它添加到 mix 中

const path = require("path");
module.exports = {
  entry: "./handler.js",
  output: {
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '.webpack'),
    filename: 'handler.js',
  },
  target: "node",
  mode: "production",
  externals: ["aws-sdk"],
  resolve: {
    mainFields: ["main"]
  }
};

注意,我们正在设置 target: node,以便 Node 特定的资产能够得到正确的处理。还要注意,您可能需要将输出文件名设置为 handler.js。我还将 aws-sdk 添加到 externals 数组中,这样 webpack 就不再捆绑它;相反,它会保留对 const AWS = require("aws-sdk"); 的调用,让它在运行时由我们的 Lamdba 处理。这是可以的,因为 Lambda 已经隐式地提供了 aws-sdk,这意味着我们不需要将它发送到网络中。最后,mainFields: ["main"] 是为了告诉 webpack 忽略任何 ESM module 字段。这对于解决 Jimp 库的一些问题是必要的。

现在让我们重新部署,希望我们能看到 webpack 运行。

现在我们的代码被很好地捆绑到一个 935K 的单个文件中,压缩后仅为 337K。节省了大量空间!

杂项

如果您想知道如何将其他数据发送到 Lambda,您可以将想要发送的内容添加到请求对象中,该对象的类型为 FormData,来自之前。例如

request.append("xyz", "Hi there");

…然后在 Lambda 中读取 formPayload.xyz。如果您需要发送安全令牌或其他文件信息,这将很有用。

如果您想知道如何为您的 Lambda 配置环境变量,您可能已经猜到,这与向您的 serverless.yaml 文件添加一些字段一样简单。它甚至支持从外部文件(可能未提交到 git)中读取值。这篇博文 由 Philipp Müns 很好地介绍了它。

总结

Serverless 是一个令人难以置信的框架。我保证,我们只是触及了皮毛。希望这篇文章向您展示了它的潜力,并激励您进一步探索它。

如果您有兴趣了解更多信息,我建议您阅读来自 Netlify 工程师 David Wells 的学习资料,他也是 serverless 团队的前成员,以及由 Swizec Teller 编写的 Serverless 手册