

# 创建无服务器文件处理应用程序
<a name="file-processing-app"></a>

Lambda 的一个常见用例是执行文件处理任务。例如，使用 Lambda 函数从 HTML 文件或图像自动创建 PDF 文件，或者在用户上传图像时创建缩略图。

在本示例中，当 PDF 文件上传至 Amazon Simple Storage Service（Amazon S3）存储桶时，创建好的应用程序会自动加密 PDF 文件。要实现此应用程序，您要创建以下资源：
+ S3 存储桶，供用户上传 PDF 文件
+ Python 中的 Lambda 函数，用于读取上传的文件并创建加密的、受密码保护的文件版本
+ 第二个 S3 存储桶，供 Lambda 保存加密文件

您还可以创建一个 AWS Identity and Access Management（IAM）策略，来授予 Lambda 函数对 S3 存储桶执行读写操作的权限。

![\[\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/ExampleApps/file_process_resources.png)


**提示**  
如果您是 Lambda 全新用户，建议先从教程[创建第一个 Lambda 函数](getting-started.md)开始，再创建此示例应用程序。

您可以使用 AWS 管理控制台 或 AWS Command Line Interface（AWS CLI）创建并配置资源来手动部署应用程序。您也可以使用 AWS Serverless Application Model（AWS SAM）来部署应用程序。AWS SAM 是基础设施即代码（IaC）工具。若借助 IaC，则不必手动创建资源，只需在代码中定义资源，就能自动部署这些资源。

若想在部署此示例应用程序之前，了解有关将 Lambda 与 IaC 结合使用的更多信息，请参阅[将 Lambda 与基础设施即代码（IaC）结合使用](foundation-iac.md)。

## 创建 Lambda 函数源代码文件
<a name="file-processing-app-download"></a>

在您的项目目录中创建以下文件：
+ `lambda_function.py` – 执行文件加密的 Lambda 函数的 Python 函数代码
+ `requirements.txt` – 定义 Python 函数代码所需的依赖项的清单文件

展开以下各部分，查看代码，继而详细了解每个文件所起的作用。要在本地计算机上创建文件，请复制并粘贴以下代码，或从 [aws-lambda-developer-guide GitHub 存储库](https://github.com/awsdocs/aws-lambda-developer-guide/tree/main/sample-apps/file-processing-python)下载文件。

### Python 函数代码
<a name="file-processing-app-function-code"></a>

将以下代码复制并粘贴到名为 `lambda_function.py` 的文件。

```
from pypdf import PdfReader, PdfWriter
import uuid
import os
from urllib.parse import unquote_plus
import boto3

# Create the S3 client to download and upload objects from S3
s3_client = boto3.client('s3')

def lambda_handler(event, context):
    # Iterate over the S3 event object and get the key for all uploaded files
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = unquote_plus(record['s3']['object']['key']) # Decode the S3 object key to remove any URL-encoded characters
        download_path = f'/tmp/{uuid.uuid4()}.pdf' # Create a path in the Lambda tmp directory to save the file to 
        upload_path = f'/tmp/converted-{uuid.uuid4()}.pdf' # Create another path to save the encrypted file to
        
        # If the file is a PDF, encrypt it and upload it to the destination S3 bucket
        if key.lower().endswith('.pdf'):
            s3_client.download_file(bucket, key, download_path)
            encrypt_pdf(download_path, upload_path)
            encrypted_key = add_encrypted_suffix(key)
            s3_client.upload_file(upload_path, f'{bucket}-encrypted', encrypted_key)

# Define the function to encrypt the PDF file with a password
def encrypt_pdf(file_path, encrypted_file_path):
    reader = PdfReader(file_path)
    writer = PdfWriter()
    
    for page in reader.pages:
        writer.add_page(page)

    # Add a password to the new PDF
    writer.encrypt("my-secret-password")

    # Save the new PDF to a file
    with open(encrypted_file_path, "wb") as file:
        writer.write(file)

# Define a function to add a suffix to the original filename after encryption
def add_encrypted_suffix(original_key):
    filename, extension = original_key.rsplit('.', 1)
    return f'{filename}_encrypted.{extension}'
```

**注意**  
在此示例代码中，加密文件 (`my-secret-password`) 的密码被硬编码到函数代码中。在生产应用程序中，切勿在函数代码中包含密码等敏感信息。相反，[创建一个 AWS Secrets Manager 密钥](https://docs.aws.amazon.com/secretsmanager/latest/userguide/create_secret.html)，然后[使用 AWS 参数和密钥 Lambda 扩展](with-secrets-manager.md)在 Lambda 函数中检索您的凭证。

Python 函数代码包含三个函数：一个是 Lambda 在调用函数时运行的[处理程序函数](python-handler.md)，以及两个名为 `add_encrypted_suffix` 和 `encrypt_pdf` 的独立函数，处理程序调用它们来执行 PDF 加密。

在 Amazon S3 调用函数时，Lambda 会将一个 JSON 格式的*事件*参数传递给函数，该参数包含有关导致调用的事件详细信息。在本例中，此类信息包括 S3 存储桶的名称和上传文件的对象键。要了解有关 Amazon S3 事件对象格式的更多信息，请参阅[使用 Lambda 处理 Amazon S3 事件通知](with-s3.md)。

然后，函数使用 适用于 Python (Boto3) 的 AWS SDK 将事件对象中指定的 PDF 文件下载到其本地临时存储目录，再使用 [https://pypi.org/project/pypdf/](https://pypi.org/project/pypdf/) 库对文件进行加密。

最后，函数使用 Boto3 SDK 将加密文件存储在 S3 目标存储桶中。

### `requirements.txt` 清单文件
<a name="file-processing-app-dependencies"></a>

将以下代码复制并粘贴到名为 `requirements.txt` 的文件。

```
boto3
pypdf
```

在本示例中，函数代码只有两个不属于标准 Python 库的依赖项：一是适用于 Python 的 SDK（Boto3），二是函数用来执行 PDF 加密的 `pypdf` 程序包。

**注意**  
适用于 Python 的 SDK（Boto3）的一个版本作为 Lambda 运行时的一部分包含在内，因此无需将 Boto3 添加到函数的部署包中，代码即可运行。不过，为了完全控制函数依赖项并避免可能出现的版本不一致问题，Python 的最佳实践是将所有函数依赖项包含在函数的部署包中。请参阅 [Python 中的运行时系统依赖项](python-package.md#python-package-dependencies)，了解更多信息。

## 部署应用程序
<a name="file-processing-app-deploy"></a>

您可以手动或使用 AWS SAM 自动创建并部署此示例应用程序的资源。在生产环境中，建议使用 AWS SAM 之类的 IaC 工具来快速、可重复地部署整个无服务器应用程序，无需采用手动流程。

### 手动部署资源
<a name="file-processing-app-deploy-manual"></a>

手动部署应用程序：
+ 创建源和目标 Amazon S3 存储桶
+ 创建一个 Lambda 函数，用于加密 PDF 文件并将加密版文件保存到 S3 存储桶
+ 配置一个 Lambda 触发器，该触发器将在对象上传到源存储桶时调用函数

在开始之前，请确保在生成计算机上安装 [Python](https://www.python.org/downloads/)。

#### 创建两个 S3 存储桶
<a name="file-processing-app-deploy-manual-create-buckets"></a>

先创建两个 S3 存储桶。第一个是源存储桶，供您向其上传 PDF 文件。第二个是目标存储桶，供 Lambda 保存调用函数时加密的文件。

------
#### [ Console ]

**创建 S3 存储桶（控制台）**

1. 打开 Amazon S3 控制台的[通用存储桶s](https://console.aws.amazon.com/s3/buckets)页面。

1. 选择最接近您地理位置的 AWS 区域。您可以使用屏幕顶部的下拉列表更改区域。  
![\[\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/console_region_select.png)

1. 选择 **Create bucket**（创建存储桶）。

1. 在 **General configuration**（常规配置）下，执行以下操作：

   1. 对于**存储桶类型**，确保选中**通用型**。

   1. 对于**存储桶名称**，输入符合 Amazon S3 [存储桶命名规则](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html)的全局唯一名称。存储桶名称只能由小写字母、数字、句点（.）和连字符（-）组成。

1. 将所有其他选项设置为默认值并选择**创建存储桶**。

1. 重复步骤 1 到 4 以创建自己的目标存储桶。在**存储桶名称**中输入 `amzn-s3-demo-bucket-encrypted`，其中 `amzn-s3-demo-bucket` 是您刚刚创建的源存储桶的名称。

------
#### [ AWS CLI ]

在开始之前，请确保在生成计算机上[安装 AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)。

**创建 Amazon S3 存储桶（AWS CLI）**

1. 运行以下 CLI 命令来创建自己的源存储桶。您为存储桶选择的名称必须具有全局唯一性，并遵守 Amazon S3 [存储桶命名规则](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html)。名称只能由小写字母、数字、句点（.）和连字符（-）组成。对于 `region` 和 `LocationConstraint`，请选择最接近您地理位置的 [AWS 区域](https://docs.aws.amazon.com/general/latest/gr/lambda-service.html)。

   ```
   aws s3api create-bucket --bucket amzn-s3-demo-bucket --region us-east-2 \
   --create-bucket-configuration LocationConstraint=us-east-2
   ```

   在本教程的后面部分，您必须在与源存储桶相同的 AWS 区域 中创建 Lambda 函数，因此请记下您选择的区域。

1. 运行以下命令来创建自己的目标存储桶。对于存储桶名称，必须使用 `amzn-s3-demo-bucket-encrypted`，其中 `amzn-s3-demo-bucket` 是您在步骤 1 中创建的源存储桶名称。对于 `region` 和 `LocationConstraint`，请选择与用于创建源存储桶时相同的 AWS 区域。

   ```
   aws s3api create-bucket --bucket amzn-s3-demo-bucket-encrypted --region us-east-2 \
   --create-bucket-configuration LocationConstraint=us-east-2
   ```

------

#### 创建执行角色
<a name="file-processing-app-deploy-manual-create-execution-role"></a>

执行角色是一个 IAM 角色，用于向 Lambda 函数授予访问 AWS 服务 和资源的权限。要授予函数对 Amazon S3 的读取和写入权限，必须附加 [AWS 托管式策略](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#aws-managed-policies) `AmazonS3FullAccess`。

------
#### [ Console ]

**创建执行角色并附加 `AmazonS3FullAccess` 托管策略（控制台）**

1. 在 IAM 控制台中，打开 [Roles](https://console.aws.amazon.com/iam/home/roles)（角色）页面。

1. 请选择 **Create role**（创建角色）。

1. 对于**可信实体类型**，选择 **AWS 服务**；对于**应用场景**，选择 **Lambda**。

1. 选择**下一步**。

1. 通过执行以下操作添加 `AmazonS3FullAccess` 托管策略：

   1. 在**权限策略**搜索框中输入 **AmazonS3FullAccess**。

   1. 选中该策略旁的复选框。

   1. 选择**下一步**。

1. 在**角色详细信息**中，对**角色名称**输入 **LambdaS3Role**。

1. 选择**创建角色**。

------
#### [ AWS CLI ]

**创建执行角色并附加 `AmazonS3FullAccess` 托管式策略（AWS CLI）**

1. 将下列 JSON 保存在名为 `trust-policy.json` 的文件中。此信任策略允许 Lambda 通过向服务主体 `lambda.amazonaws.com` 授予调用 AWS Security Token Service（AWS STS）`AssumeRole` 操作的权限来使用该角色的权限。  
****  

   ```
   {
     "Version":"2012-10-17",		 	 	 
     "Statement": [
       {
         "Effect": "Allow",
         "Principal": {
           "Service": "lambda.amazonaws.com"
         },
         "Action": "sts:AssumeRole"
       }
     ]
   }
   ```

1. 在保存 JSON 信任策略文档的目录中，运行以下 CLI 命令来创建执行角色。

   ```
   aws iam create-role --role-name LambdaS3Role --assume-role-policy-document file://trust-policy.json
   ```

1. 要附加 `AmazonS3FullAccess` 托管式策略，请运行下列 CLI 命令。

   ```
   aws iam attach-role-policy --role-name LambdaS3Role --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
   ```

------

#### 创建函数部署包
<a name="file-processing-app-deploy-manual-create-function-package"></a>

要创建函数，您需要创建包含函数代码和所有依赖项的*部署包*。对于此应用程序，函数代码使用单独的库来加密 PDF。

**创建部署包**

1. 导航到包含之前创建的或从 GitHub 下载的 `lambda_function.py` 和 `requirements.txt` 文件的项目目录，然后创建一个名为 `package` 的新目录。

1. 运行以下命令，将 `requirements.txt` 文件中指定的依赖项安装到 `package` 目录中。

   ```
   pip install -r requirements.txt --target ./package/
   ```

1. 创建一个包含应用程序代码及其依赖项的 .zip 文件。在 Linux 或 MacOS 中，从命令行界面运行以下命令。

   ```
   cd package
   zip -r ../lambda_function.zip .
   cd ..
   zip lambda_function.zip lambda_function.py
   ```

    在 Windows 中，使用您首选的压缩工具来创建 `lambda_function.zip` 文件。确保您的 `lambda_function.py` 文件和包含依赖项的文件夹都位于.zip 文件的根目录下。

您也可以使用 Python 虚拟环境创建部署包。请参阅 [将 .zip 文件归档用于 Python Lambda 函数](python-package.md)。

#### 创建 Lambda 函数
<a name="file-processing-app-deploy-manual-createfunction"></a>

现在，您可以使用在上一步中创建的部署包来部署 Lambda 函数。

------
#### [ Console ]

**创建函数（控制台）**

要使用控制台创建 Lambda 函数，首先要创建包含一些“Hello world”代码的基本函数。然后，通过上传在上一步中创建的 .zip 文件，将此代码替换为自己的函数代码。

为确保加密大型 PDF 文件时函数不会超时，必须配置该函数的内存和超时设置。您还要将函数的日志格式设置为 JSON。使用提供的测试脚本时，必须配置 JSON 格式的日志，以便其可以从 CloudWatch Logs 中读取函数的调用状态，从而确认是否成功调用。

1. 打开 Lamba 控制台的[函数](https://console.aws.amazon.com/lambda/home#/functions)页面。

1. 确保您在创建 S3 存储桶所在的同一 AWS 区域 内操作。您可以使用屏幕顶部的下拉列表更改区域。  
![\[\]](http://docs.aws.amazon.com/zh_cn/lambda/latest/dg/images/console_region_select.png)

1. 选择**创建函数**。

1. 选择**从头开始创作**。

1. 在**基本信息**中，执行以下操作：

   1. 对于 **Function name（函数名称）**，请输入 `EncryptPDF`。

   1. 对于**运行时**，选择 **Python 3.12**。

   1. 对于**架构**，选择 **x86\$164**。

1. 通过执行以下操作，附加您在上一步中创建的执行角色：

   1. 展开**更改默认执行角色**部分。

   1. 选择**使用现有角色**。

   1. 在**现有角色**下，选择您的角色（`LambdaS3Role`）。

1. 选择**创建函数**。

**上传函数代码（控制台）**

1. 在**代码源**窗格中，选择**上传自**。

1. 选择 **.zip 文件**。

1. 选择**上传**。

1. 在文件选择器中，选择 .zip 文件，然后选择**打开**。

1. 选择**保存**。

**配置函数内存和超时（控制台）**

1. 选择函数的**配置**选项卡。

1. 在**常规配置**窗格中，选择**编辑**。

1. 将**内存**设置为 256 MB，并将**超时**设置为 15 秒。

1. 选择**保存**。

**配置日志格式（控制台）**

1. 选择函数的**配置**选项卡。

1. 选择**监控和操作工具**。

1. 在**日志记录配置**窗格中，选择**编辑**。

1. 对于**日志记录配置**，选择 **JSON**。

1. 选择**保存**。

------
#### [ AWS CLI ]

**创建函数（AWS CLI）**
+ 从包含 `lambda_function.zip` 文件的目录中运行以下命令。对于 `region` 参数，将 `us-east-2` 替换为创建 S3 存储桶时使用的区域。

  ```
  aws lambda create-function --function-name EncryptPDF \
  --zip-file fileb://lambda_function.zip --handler lambda_function.lambda_handler \
  --runtime python3.12 --timeout 15 --memory-size 256 \
  --role arn:aws:iam::123456789012:role/LambdaS3Role --region us-east-2 \
  --logging-config LogFormat=JSON
  ```

------

#### 配置 Amazon S3 触发器来调用函数
<a name="file-processing-app-deploy-manual-configure-s3-trigger"></a>

为了在将文件上传到源存储桶时运行 Lambda 函数，您需要为函数配置触发器。您可以使用控制台或 AWS CLI 配置 Amazon S3 触发器。

**重要**  
此程序将 S3 存储桶配置为每次在该存储桶中创建对象时调用您的函数。请确保仅在源存储桶上配置。如果您的 Lambda 函数在调用此函数的同一个存储桶中创建对象，则可以[在循环中持续调用](https://serverlessland.com/content/service/lambda/guides/aws-lambda-operator-guide/recursive-runaway)您的函数。这可能会导致您的 AWS 账户 产生额外费用。

------
#### [ Console ]

**配置 Amazon S3 触发器（控制台）**

1. 打开 Lambda 控制台的[函数页面](https://console.aws.amazon.com/lambda/home#/functions)，然后选择函数 (`EncryptPDF`)。

1. 选择**添加触发器**。

1. 选择 **S3**。

1. 在**存储桶**下，选择自己的源存储桶。

1. 在**事件类型**下，选择**所有对象创建事件**。

1. 在**递归调用**下，选中复选框以确认知晓不建议使用相同的 S3 存储桶用于输入和输出。您可以阅读 Serverless Land 中的 [Recursive patterns that cause run-away Lambda functions](https://serverlessland.com/content/service/lambda/guides/aws-lambda-operator-guide/recursive-runaway)，进一步了解 Lambda 中的递归调用模式。

1. 选择**添加**。

   在您使用 Lambda 控制台创建触发器时，Lambda 会自动创建[基于资源的策略](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html)，授予您选择的服务调用函数的权限。

------
#### [ AWS CLI ]

**配置 Amazon S3 触发器（AWS CLI）**

1. 向函数中添加[基于资源的策略](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html)，以允许 Amazon S3 源存储桶在添加文件时调用函数。基于资源的策略声明授予其他 AWS 服务 调用您函数的权限。要授予 Amazon S3 调用函数的权限，请运行以下 CLI 命令。请务必将 `source-account` 参数替换为您的 AWS 账户 ID 并使用自己的源存储桶名称。

   ```
   aws lambda add-permission --function-name EncryptPDF \
   --principal s3.amazonaws.com --statement-id s3invoke --action "lambda:InvokeFunction" \
   --source-arn arn:aws:s3:::amzn-s3-demo-bucket \
   --source-account 123456789012
   ```

   您使用此命令定义的策略允许 Amazon S3 仅在源存储桶上执行操作时调用函数。
**注意**  
虽然 S3 存储桶名称具有全局唯一性，但在使用基于资源的策略时，最佳做法是指定存储桶必须属于您的账户。这是因为，如果删除一个存储桶，则另一个 AWS 账户 账户可能会创建具有相同 Amazon 资源名称（ARN）的存储桶。

1. 将下列 JSON 保存在名为 `notification.json` 的文件中。在应用到您的源存储桶时，此 JSON 会将存储桶配置为在每次添加新对象时向 Lambda 函数发送通知。将 Lambda 函数 ARN 中的 AWS 账户 号码和 AWS 区域 替换为您自己的账号和区域。

   ```
   {
   "LambdaFunctionConfigurations": [
       {
         "Id": "EncryptPDFEventConfiguration",
         "LambdaFunctionArn": "arn:aws:lambda:us-east-2:123456789012:function:EncryptPDF",
         "Events": [ "s3:ObjectCreated:Put" ]
       }
     ]
   }
   ```

1. 运行以下 CLI 命令，将您创建的 JSON 文件中的通知设置应用到源存储桶。将 `amzn-s3-demo-bucket` 替换为源存储桶的名称。

   ```
   aws s3api put-bucket-notification-configuration --bucket amzn-s3-demo-bucket \
   --notification-configuration file://notification.json
   ```

   要了解有关 `put-bucket-notification-configuration` 命令和 `notification-configuration` 选项的更多信息，请参阅 *AWS CLI 命令参考*中的 [put-bucket-notification-configuration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3api/put-bucket-notification-configuration.html)。

------

### 使用 AWS SAM 来部署资源
<a name="file-processing-app-deploy-sam"></a>

在开始之前，请确保您的生成计算机上安装了 [Docker](https://docs.docker.com/get-docker/) 和[最新版本的 AWS SAMCLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)。

1. 在项目目录中，将以下代码复制并粘贴到名为 `template.yaml` 的文件中。替换占位符存储桶名称：
   + 对于源存储桶，请将 `amzn-s3-demo-bucket` 替换为符合 [S3 存储桶命名规则](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html)的任意名称。
   + 对于目标存储桶，将 `amzn-s3-demo-bucket-encrypted` 替换为 `<source-bucket-name>-encrypted`，其中 `<source-bucket>` 是您为源存储桶选择的名称。

   ```
   AWSTemplateFormatVersion: '2010-09-09'
   Transform: AWS::Serverless-2016-10-31
   
   Resources:
     EncryptPDFFunction:
       Type: AWS::Serverless::Function
       Properties:
         FunctionName: EncryptPDF
         Architectures: [x86_64]
         CodeUri: ./
         Handler: lambda_function.lambda_handler
         Runtime: python3.12
         Timeout: 15
         MemorySize: 256
         LoggingConfig:
           LogFormat: JSON
         Policies:
           - AmazonS3FullAccess
         Events:
           S3Event:
             Type: S3
             Properties:
               Bucket: !Ref PDFSourceBucket
               Events: s3:ObjectCreated:*
   
     PDFSourceBucket:
       Type: AWS::S3::Bucket
       Properties:
         BucketName: amzn-s3-demo-bucket
   
     EncryptedPDFBucket:
       Type: AWS::S3::Bucket
       Properties:
         BucketName: amzn-s3-demo-bucket-encrypted
   ```

   AWS SAM 模板定义了您为应用程序创建的资源。在本示例中，模板使用 `AWS::Serverless::Function` 类型定义一个 Lambda 函数，并使用 `AWS::S3::Bucket` 类型定义两个 S3 存储桶。模板中指定的存储桶名称是占位符。在使用 AWS SAM 部署应用程序之前，您需要编辑模板，使用符合 [S3 存储桶命名规则](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html)的全局唯一名称重命名存储桶。[使用 AWS SAM 来部署资源](#file-processing-app-deploy-sam) 中将进一步介绍该步骤。

   Lambda 函数资源的定义使用 `S3Event` 事件属性为函数配置触发器。只要在源存储桶中创建对象，该触发器就会调用函数。

   函数定义还指定了要附加到函数[执行角色](lambda-intro-execution-role.md)的 AWS Identity and Access Management（IAM）策略。[AWS 托管式策略](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#aws-managed-policies) `AmazonS3FullAccess` 授予函数在 Amazon S3 中读取和写入对象所需的权限。

1. 在保存 `template.yaml`、`lambda_function.py` 和 `requirements.txt` 文件的目录中，运行以下命令。

   ```
   sam build --use-container
   ```

   此命令会收集应用程序的构建构件，并将其以适当的格式放置在适当的位置进行部署。指定 `--use-container` 选项会在类似 Lambda 的 Docker 容器中构建函数。我们在这里使用它，您无需在本地计算机上安装 Python 3.12 即可进行构建。

   在构建过程中，AWS SAM 会在以模板中的 `CodeUri` 属性指定的位置中查找 Lambda 函数代码。在本例中，我们将当前目录指定为位置（`./`）。

   如果存在 `requirements.txt` 文件，则 AWS SAM 使用该文件来收集指定的依赖项。默认情况下，AWS SAM 会创建包含函数代码和依赖项的 .zip 部署包。您也可以选择使用 [PackageType](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-packagetype) 属性将函数部署为容器映像。

1. 要部署应用程序并创建 AWS SAM 模板中指定的 Lambda 和 Amazon S3 资源，请运行以下命令。

   ```
   sam deploy --guided
   ```

   使用 `--guided` 标志意味着 AWS SAM 将向您显示提示，以指导您完成部署过程。对于此部署，请按 Enter 接受默认选项。

在部署过程中，AWS SAM 将在 AWS 账户 中创建以下资源：
+ 一个名为 `sam-app` 的 CloudFormation [堆栈](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-whatis-concepts.html#cfn-concepts-stacks)
+ 一个名为 `EncryptPDF` 的 Lambda 函数
+ 两个 S3 存储桶，其名称为编辑 `template.yaml` AWS SAM 模板文件时选择的名称
+ 一个函数的 IAM 执行角色，名称格式为 `sam-app-EncryptPDFFunctionRole-2qGaapHFWOQ8`

AWS SAM 创建完资源后，您将看到以下消息：

```
Successfully created/updated stack - sam-app in us-east-2
```

## 测试应用程序
<a name="file-processing-app-test"></a>

要测试应用程序，请将 PDF 文件上传到源存储桶，然后确认 Lambda 已在目标存储桶中创建加密版文件。在本示例中，您可以使用控制台或 AWS CLI 对此进行手动测试，也可以使用提供的测试脚本进行自动测试。

对于生产应用程序，可以使用单元测试等传统的测试方法和技术，来确认 Lambda 函数代码是否正常运行。最佳实践也是执行与所提供测试脚本中的测试类似的测试，这些测试使用真实的基于云的资源执行集成测试。在云端中执行集成测试可确认基础架构是否已正确部署，并确认事件是否按预期在不同的服务之间流动。要了解更多信息，请参阅[如何测试无服务器函数和应用程序](testing-guide.md)。

### 手动测试应用程序
<a name="file-processing-app-test-manual"></a>

您可以将 PDF 文件添加到 Amazon S3 源存储桶来手动测试函数。将文件添加到源存储桶时，应自动调用 Lambda 函数，并应在目标存储桶中存储加密版文件。

------
#### [ Console ]

**通过上传文件来测试应用程序（控制台）**

1. 要将 PDF 文件上传到 S3 存储桶，请执行以下操作：

   1. 打开 Amazon S3 控制台的[存储桶](https://console.aws.amazon.com/s3/buckets)页面，然后选择您的源存储桶。

   1. 选择**上传**。

   1. 选择**添加文件**，然后使用文件选择器选择要上传的 PDF 文件。

   1. 选择**打开**，然后选择**上传**。

1. 执行以下操作，验证 Lambda 是否已将加密版 PDF 文件保存在目标存储桶中：

   1. 导航回 Amazon S3 控制台的[存储桶](https://console.aws.amazon.com/s3/buckets)页面，然后选择您的目标存储桶。

   1. 在**对象**窗格中，现在应该可以到一个名称格式为 `filename_encrypted.pdf` 的文件（其中 `filename.pdf` 是已上传到源存储桶的文件的名称）。要下载加密的 PDF，请选择所需文件，然后选择**下载**。

   1. 确认是否可以使用 Lambda 函数保护的密码 (`my-secret-password`) 打开下载的文件。

------
#### [ AWS CLI ]

**通过上传文件来测试应用程序（AWS CLI）**

1. 在包含要上传的 PDF 文件的目录中，运行以下 CLI 命令。将 `--bucket` 参数替换为源存储桶的名称。对于 `--key` 和 `--body` 参数，请使用测试文件的文件名。

   ```
   aws s3api put-object --bucket amzn-s3-demo-bucket --key test.pdf --body ./test.pdf
   ```

1. 验证函数是否已创建加密版文件并已保存到目标 S3 存储桶中。运行以下 CLI 命令，将 `amzn-s3-demo-bucket-encrypted` 替换为自己的目标存储桶的名称。

   ```
   aws s3api list-objects-v2 --bucket amzn-s3-demo-bucket-encrypted
   ```

   如果函数成功运行，您将看到类似于以下内容的输出。目标存储桶应包含名称格式为 `<your_test_file>_encrypted.pdf` 的文件，其中 `<your_test_file>` 是已上传文件的名称。

   ```
   {
       "Contents": [
           {
               "Key": "test_encrypted.pdf",
               "LastModified": "2023-06-07T00:15:50+00:00",
               "ETag": "\"7781a43e765a8301713f533d70968a1e\"",
               "Size": 2763,
               "StorageClass": "STANDARD"
           }
       ]
   }
   ```

1. 要下载 Lambda 保存在目标存储桶中的文件，请运行以下 CLI 命令。将 `--bucket` 参数替换为目标存储桶的名称。对于 `--key` 参数，请使用文件名 `<your_test_file>_encrypted.pdf`，其中 `<your_test_file>` 是已上传的测试文件的名称。

   ```
   aws s3api get-object --bucket amzn-s3-demo-bucket-encrypted --key test_encrypted.pdf my_encrypted_file.pdf
   ```

   该命令会将文件下载到当前目录并将其另存为 `my_encrypted_file.pdf`。

1. 确认是否可以使用 Lambda 函数保护的密码 (`my-secret-password`) 打开下载的文件。

------

### 使用自动化脚本测试应用程序
<a name="file-processing-app-test-auto"></a>

在您的项目目录中创建以下文件：
+ `test_pdf_encrypt.py` – 可用于自动测试应用程序的测试脚本
+ `pytest.ini` – 测试脚本的配置文件

展开以下各部分，查看代码，继而详细了解每个文件所起的作用。

#### 自动化测试脚本
<a name="file-processing-app-test-script"></a>

将以下代码复制并粘贴到名为 `test_pdf_encrypt.py` 的文件。请务必替换占位符存储桶名称：
+ 在 `test_source_bucket_available` 函数中，将 `amzn-s3-demo-bucket` 替换为源存储桶的名称。
+ 在 `test_encrypted_file_in_bucket` 函数中，将 `amzn-s3-demo-bucket-encrypted` 替换为 `source-bucket-encrypted`，其中 `source-bucket>` 是源存储桶的名称。
+ 在 `cleanup` 函数中，将 `amzn-s3-demo-bucket` 替换为源存储桶的名称，并将 `amzn-s3-demo-bucket-encrypted` 替换为目标存储桶的名称。

```
import boto3
import json
import pytest
import time
import os

@pytest.fixture
def lambda_client():
    return boto3.client('lambda')
    
@pytest.fixture
def s3_client():
    return boto3.client('s3')

@pytest.fixture
def logs_client():
    return boto3.client('logs')

@pytest.fixture(scope='session')
def cleanup():
    # Create a new S3 client for cleanup
    s3_client = boto3.client('s3')

    yield
    # Cleanup code will be executed after all tests have finished

    # Delete test.pdf from the source bucket
    source_bucket = 'amzn-s3-demo-bucket'
    source_file_key = 'test.pdf'
    s3_client.delete_object(Bucket=source_bucket, Key=source_file_key)
    print(f"\nDeleted {source_file_key} from {source_bucket}")

    # Delete test_encrypted.pdf from the destination bucket
    destination_bucket = 'amzn-s3-demo-bucket-encrypted'
    destination_file_key = 'test_encrypted.pdf'
    s3_client.delete_object(Bucket=destination_bucket, Key=destination_file_key)
    print(f"Deleted {destination_file_key} from {destination_bucket}")
        

@pytest.mark.order(1)
def test_source_bucket_available(s3_client):
    s3_bucket_name = 'amzn-s3-demo-bucket'
    file_name = 'test.pdf'
    file_path = os.path.join(os.path.dirname(__file__), file_name)

    file_uploaded = False
    try:
        s3_client.upload_file(file_path, s3_bucket_name, file_name)
        file_uploaded = True
    except:
        print("Error: couldn't upload file")

    assert file_uploaded, "Could not upload file to S3 bucket"

    

@pytest.mark.order(2)
def test_lambda_invoked(logs_client):

    # Wait for a few seconds to make sure the logs are available
    time.sleep(5)

    # Get the latest log stream for the specified log group
    log_streams = logs_client.describe_log_streams(
        logGroupName='/aws/lambda/EncryptPDF',
        orderBy='LastEventTime',
        descending=True,
        limit=1
    )

    latest_log_stream_name = log_streams['logStreams'][0]['logStreamName']

    # Retrieve the log events from the latest log stream
    log_events = logs_client.get_log_events(
        logGroupName='/aws/lambda/EncryptPDF',
        logStreamName=latest_log_stream_name
    )

    success_found = False
    for event in log_events['events']:
        message = json.loads(event['message'])
        status = message.get('record', {}).get('status')
        if status == 'success':
            success_found = True
            break

    assert success_found, "Lambda function execution did not report 'success' status in logs."

@pytest.mark.order(3)
def test_encrypted_file_in_bucket(s3_client):
    # Specify the destination S3 bucket and the expected converted file key
    destination_bucket = 'amzn-s3-demo-bucket-encrypted'
    converted_file_key = 'test_encrypted.pdf'

    try:
        # Attempt to retrieve the metadata of the converted file from the destination S3 bucket
        s3_client.head_object(Bucket=destination_bucket, Key=converted_file_key)
    except s3_client.exceptions.ClientError as e:
        # If the file is not found, the test will fail
        pytest.fail(f"Converted file '{converted_file_key}' not found in the destination bucket: {str(e)}")

def test_cleanup(cleanup):
    # This test uses the cleanup fixture and will be executed last
    pass
```

自动化测试脚本将执行三个测试函数来确认应用程序运行是否正确：
+ 测试 `test_source_bucket_available` 通过将测试 PDF 文件上传到存储桶来确认源存储桶是否已成功创建。
+ 测试 `test_lambda_invoked` 通过询问函数的最新 CloudWatch Logs 日志流，来确认上传测试文件时，Lambda 函数是否已运行并报告成功。
+ 测试 `test_encrypted_file_in_bucket` 确认目标存储桶是否包含加密 `test_encrypted.pdf` 文件。

待这些测试运行完毕后，脚本会执行额外的清理步骤，以便从源存储桶和目标存储桶中删除 `test.pdf` 和 `test_encrypted.pdf` 文件。

与 AWS SAM 模板一样，此文件中指定的存储桶名称是占位符。在运行测试前，必须使用应用程序的真实存储桶名称编辑此文件。[使用自动化脚本测试应用程序](#file-processing-app-test-auto) 中将进一步介绍此步骤

#### 测试脚本配置文件
<a name="file-processing-app-test-config"></a>

将以下代码复制并粘贴到名为 `pytest.ini` 的文件。

```
[pytest]
markers =
    order: specify test execution order
```

这是为了指定 `test_pdf_encrypt.py` 脚本中测试运行的顺序。

要运行测试，请执行以下操作：

1. 确保已在本地环境中安装 `pytest` 模块。可以通过运行以下命令安装 `pytest`：

   ```
   pip install pytest
   ```

1. 将名为 `test.pdf` 的 PDF 文件保存在包含 `test_pdf_encrypt.py` 和 `pytest.ini` 文件的目录中。

1. 打开终端或 Shell 程序，从包含测试文件的目录中运行以下命令。

   ```
   pytest -s -v
   ```

   测试完成后，输出应与以下内容类似：

   ```
   ============================================================== test session starts =========================================================
   platform linux -- Python 3.12.2, pytest-7.2.2, pluggy-1.0.0 -- /usr/bin/python3
   cachedir: .pytest_cache
   hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('/home/pdf_encrypt_app/.hypothesis/examples')
   Test order randomisation NOT enabled. Enable with --random-order or --random-order-bucket=<bucket_type>
   rootdir: /home/pdf_encrypt_app, configfile: pytest.ini
   plugins: anyio-3.7.1, hypothesis-6.70.0, localserver-0.7.1, random-order-1.1.0
   collected 4 items
   
   test_pdf_encrypt.py::test_source_bucket_available PASSED
   test_pdf_encrypt.py::test_lambda_invoked PASSED
   test_pdf_encrypt.py::test_encrypted_file_in_bucket PASSED
   test_pdf_encrypt.py::test_cleanup PASSED
   Deleted test.pdf from amzn-s3-demo-bucket
   Deleted test_encrypted.pdf from amzn-s3-demo-bucket-encrypted
   
   
   =============================================================== 4 passed in 7.32s ==========================================================
   ```

## 后续步骤
<a name="file-processing-app-next-steps"></a>

您已经创建了示例应用程序，现在可以在所提供代码的基础上创建其他类型的文件处理应用程序。修改 `lambda_function.py` 文件中的代码，为用例实现文件处理逻辑。

许多常见的文件处理用例都涉及图像处理。使用 Python 时，[pillow](https://pypi.org/project/pillow/) 等常用的图像处理库通常包含 C 或 C\$1\$1 组件。为了确保函数的部署包与 Lambda 执行环境兼容，请务必使用正确的源分发二进制文件。

使用 AWS SAM 来部署资源时，必须额外采取一些步骤，以便在部署包中包含正确的源分发。由于 AWS SAM 不会为与生成计算机不同的平台安装依赖项，因此，如果生成机器使用与 Lambda 执行环境不同的操作系统或架构，则在 `requirements.txt` 文件中指定正确的源分发（`.whl` 文件）将不适用。所以，应执行以下操作之一：
+ 运行 `sam build` 时使用 `--use-container` 选项。指定此选项时，AWS SAM 会下载与 Lambda 执行环境兼容的容器基础映像，并使用该映像在 Docker 容器中构建函数的部署包。要了解更多信息，请参阅 [Building a Lambda function inside of a provided container](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/using-sam-cli-build.html#using-sam-cli-build-options-container)。
+ 使用正确的源分发二进制文件自行构建函数的 .zip 部署包，并将 .zip 文件保存在 AWS SAM 模板中指定为 `CodeUri` 的目录中。要了解有关使用二进制分发文件为 Python 构建.zip 部署包的更多信息，请参阅[创建含依赖项的 .zip 部署包](python-package.md#python-package-create-dependencies)和[使用原生库创建 .zip 部署包](python-package.md#python-package-native-libraries)。