

# 使用 Puppeteer 运行时编写 Node.js Canary 脚本
<a name="CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs_Pup"></a>

**Topics**
+ [从 Scratch 创建 CloudWatch Synthetics 金丝雀](#CloudWatch_Synthetics_Canaries_write_from_scratch)
+ [将 Node.js 金丝雀文件打包](#CloudWatch_Synthetics_Canaries_package)
+ [更改现有 Puppeteer 脚本以将其用作 Synthetics 金丝雀](#CloudWatch_Synthetics_Canaries_modify_puppeteer_script)
+ [环境变量](#CloudWatch_Synthetics_Environment_Variables)
+ [将您的金丝雀与其他 AWS 服务集成](#CloudWatch_Synthetics_Canaries_AWS_integrate)
+ [强制金丝雀使用静态 IP 地址](#CloudWatch_Synthetics_Canaries_staticIP)

## 从 Scratch 创建 CloudWatch Synthetics 金丝雀
<a name="CloudWatch_Synthetics_Canaries_write_from_scratch"></a>

这里是一个极简 Synthetics 金丝雀脚本示例。此脚本将成功通过一次运行，并返回一个字符串。要查看失败的金丝雀示例，请将 `let fail = false;` 更改为 `let fail = true;`。

您必须为金丝雀脚本定义入口点函数。要查看如何将文件上传到指定作为 Canary 的 `ArtifactS3Location` 的 Amazon S3 位置，请在 `/tmp` 文件夹下创建这些文件。所有 Canary 构件都应存储在 `/tmp` 中，因为这是唯一可写入的目录。请务必将脚本创建的所有屏幕截图或其他文件的屏幕截图路径设置为 `/tmp`。Synthetics 会自动将 ` /tmp` 中的文件上传到 S3 存储桶。

```
/tmp/<name>
```

脚本运行后，将“通过/失败”状态和持续时间指标发布到 CloudWatch，并将 `/tmp` 下的文件上传到 S3 存储桶。

```
const basicCustomEntryPoint = async function () {

    // Insert your code here

    // Perform multi-step pass/fail check

    // Log decisions made and results to /tmp

    // Be sure to wait for all your code paths to complete 
    // before returning control back to Synthetics.
    // In that way, your canary will not finish and report success
    // before your code has finished executing

    // Throw to fail, return to succeed
    let fail = false;
    if (fail) {
        throw "Failed basicCanary check.";
    }

    return "Successfully completed basicCanary checks.";
};

exports.handler = async () => {
    return await basicCustomEntryPoint();
};
```

接下来，我们将扩展脚本以使用 Synthetics 日志记录并使用 AWS SDK 进行调用。出于演示目的，此脚本将创建 Amazon DynamoDB 客户端并调用 DynamoDB listTables API。它会记录对请求的响应，并根据请求是否成功来记录通过还是失败。

```
const log = require('@aws/synthetics-logger');
const AWS = require('aws-sdk');
// Require any dependencies that your script needs
// Bundle additional files and dependencies into a .zip file with folder structure
// nodejs/node_modules/additional files and folders

const basicCustomEntryPoint = async function () {

    log.info("Starting DynamoDB:listTables canary.");
    
    let dynamodb = new AWS.DynamoDB();
    var params = {};
    let request = await dynamodb.listTables(params);
    try {
        let response = await request.promise();
        log.info("listTables response: " + JSON.stringify(response));
    } catch (err) {
        log.error("listTables error: " + JSON.stringify(err), err.stack);
        throw err;
    }

    return "Successfully completed DynamoDB:listTables canary.";
};

exports.handler = async () => {
    return await basicCustomEntryPoint();
};
```

## 将 Node.js 金丝雀文件打包
<a name="CloudWatch_Synthetics_Canaries_package"></a>

 **对于 syn-nodejs-puppeteer-11.0 及以上版本** 

 较新版本仍然支持较旧的打包结构（适用于 syn-nodejs-puppeteer-10.0 及以下版本）。

使用以下某个选项创建脚本：
+ .js 文件（CommonJS 语法）
+ .mjs 文件（ES 模块语法）

对于 ES 模块，请使用以下某个选项：
+ .js 文件（CommonJS 语法）
+ .mjs 文件（ES 模块语法）

软件包结构定义如下：
+ 根级别处理程序文件（index.js/index.mjs）
+ 可选配置文件（synthetics.json）
+ node\$1modules 中的其他依赖项（如果需要）

打包结构示例：

```
  my_function/
├── index.mjs
├── synthetics.json
├── helper-utils.mjs
└── node_modules/
    └── dependencies
```

要进行打包，请按照以下步骤操作：

1. 安装依赖项（如有）。

   ```
   npm install
   ```

1. 创建一个 .zip 包。

   ```
   zip -r my_deployment_package.zip
   ```

 **对于 syn-nodejs-puppeteer-11.0 及以下版本** 

在使用 Amazon S3 时需要以下结构：

```
  nodejs/
└── node_modules/
    └── myCanaryFilename.js
```

 **要在 syn-nodejs-puppeteer-3.4\$1 中添加可选的子文件夹支持：**

```
nodejs/
└── node_modules/
    └── myFolder/
        └── myCanaryFilename.js
```

**注意**  
配置中的处理程序路径必须与文件位置匹配。

 **处理程序名称** 

请务必将您的金丝雀脚本入口点（处理程序）设置为 ` myCanaryFilename.functionName`，以匹配脚本入口点的文件名。如果您使用 `syn-nodejs-puppeteer-3.4` 之前版本的运行时，则 `functionName` 必须是 `handler`。如果您使用 ` syn-nodejs-puppeteer-3.4` 或之后版本，您可以选择任何函数名称作为处理程序。如果您使用 `syn-nodejs-puppeteer-3.4` 或之后版本，您也可以选择将金丝雀存储在单独的文件夹中，例如 ` nodejs/node_modules/myFolder/my_canary_filename`。如果将其存储在单独的文件夹中，请在脚本入口点中指定该路径，例如 ` myFolder/my_canary_filename.functionName`。

## 更改现有 Puppeteer 脚本以将其用作 Synthetics 金丝雀
<a name="CloudWatch_Synthetics_Canaries_modify_puppeteer_script"></a>

本节介绍如何对 Puppeteer 脚本进行修改，以将其作为 Synthetics 金丝雀脚本运行。有关 Puppeteer 的更多信息，请参阅 [Puppeteer API v1.14.0](https://github.com/puppeteer/puppeteer/blob/v1.14.0/docs/api.md)。

我们从这个示例 Puppeteer 开始：

```
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();
```

转换步骤如下：
+ 创建和导出 `handler` 函数。处理程序是脚本的入口点函数。如果您使用 ` syn-nodejs-puppeteer-3.4` 之前版本的运行时，处理程序函数必须命名为 `handler`。如果您使用 `syn-nodejs-puppeteer-3.4` 或之后版本，函数可以有任何名称，但必须与脚本中使用的名称相同。此外，如果您使用 `syn-nodejs-puppeteer-3.4` 或之后版本，您可以将脚本存储在任何文件夹下，并将该文件夹指定为处理程序名称的一部分。

  ```
  const basicPuppeteerExample = async function () {};
  
  exports.handler = async () => {
      return await basicPuppeteerExample();
  };
  ```
+ 使用 `Synthetics` 依赖项。

  ```
  var synthetics = require('@aws/synthetics-puppeteer');
  ```
+ 使用 `Synthetics.getPage` 函数获取 Puppeteer `Page` 对象。

  ```
  const page = await synthetics.getPage();
  ```

  Synthetics.getPage 函数返回的页面对象指示需要记录 **page.on** `request`、`response` 和 ` requestfailed` 事件。Synthetics 还为页面上的请求和响应设置 HAR 文件生成，并将金丝雀 ARN 添加到页面上的传出请求的 user-agent 标头。

该脚本现已准备好作为 Synthetics 金丝雀运行。更新的脚本如下：

```
var synthetics = require('@aws/synthetics-puppeteer');  // Synthetics dependency

const basicPuppeteerExample = async function () {
    const page = await synthetics.getPage(); // Get instrumented page from Synthetics
    await page.goto('https://example.com');
    await page.screenshot({path: '/tmp/example.png'}); // Write screenshot to /tmp folder
};

exports.handler = async () => {  // Exported handler function 
    return await basicPuppeteerExample();
};
```

## 环境变量
<a name="CloudWatch_Synthetics_Environment_Variables"></a>

您可以在创建金丝雀时使用环境变量。这样，您就能够编写单个金丝雀脚本，然后将该脚本与不同的值相结合，快速创建具有类似任务的多个金丝雀。

例如，假定您的企业具有用于不同软件开发阶段的 `prod`、` dev` 和 `pre-release` 端点，而您需要创建金丝雀来测试其中每个端点。您可以编写一个测试软件的金丝雀脚本，然后在分别创建三个金丝雀时为端点环境变量指定不同的值。之后，在创建金丝雀时，您可以指定要用于环境变量的脚本和值。

环境变量的名称可以包含字母、数字和下划线字符，必须以字母开头，并且至少为两个字符。环境变量的总大小不能超过 4 KB。不能将任何 Lambda 预留环境变量指定为环境变量的名称。有关预留环境变量的更多信息，请参阅[运行时环境变量](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime)。

**重要**  
使用 AWS 自有 AWS KMS 密钥对环境变量密钥和值进行静态加密。但是，并未在客户端对环境变量进行加密。请勿在其中存储敏感信息。

下面的示例脚本使用两个环境变量。此脚本用于检查网页是否可用的金丝雀。该金丝雀使用环境变量来参数化所检查的 URL 和所使用的 CloudWatch Synthetics 日志级别。

以下函数将 `LogLevel` 设置为 ` LOG_LEVEL` 环境变量的值。

```
 synthetics.setLogLevel(process.env.LOG_LEVEL);
```

此函数将 `URL` 设置为 `URL` 环境变量的值。

```
const URL = process.env.URL;
```

以下是完整的脚本。当您使用此脚本创建金丝雀时，您可以指定 `LOG_LEVEL` 和 `URL` 环境变量的值。

```
var synthetics = require('@aws/synthetics-puppeteer');
const log = require('@aws/synthetics-logger');

const pageLoadEnvironmentVariable = async function () {

    // Setting the log level (0-3)
    synthetics.setLogLevel(process.env.LOG_LEVEL);
    // INSERT URL here
    const URL = process.env.URL;

    let page = await synthetics.getPage();
    //You can customize the wait condition here. For instance,
    //using 'networkidle2' may be less restrictive.
    const response = await page.goto(URL, {waitUntil: 'domcontentloaded', timeout: 30000});
    if (!response) {
        throw "Failed to load page!";
    }
    //Wait for page to render.
    //Increase or decrease wait time based on endpoint being monitored.
    await page.waitFor(15000);
    await synthetics.takeScreenshot('loaded', 'loaded');
    let pageTitle = await page.title();
    log.info('Page title: ' + pageTitle);
    log.debug('Environment variable:' + process.env.URL);

    //If the response status code is not a 2xx success code
    if (response.status() < 200 || response.status() > 299) {
        throw "Failed to load page!";
    }
};

exports.handler = async () => {
    return await pageLoadEnvironmentVariable();
};
```

### 将环境变量传递到脚本
<a name="CloudWatch_Synthetics_Canaries_pass_variables"></a>

要在控制台中创建金丝雀时将环境变量传递给脚本，请在控制台上的 **Environment variables（环境变量）**部分指定环境变量的密钥和值。有关更多信息，请参阅 [创建金丝雀](CloudWatch_Synthetics_Canaries_Create.md)。

要通过 API 或 AWS CLI 传递环境变量，请使用 `RunConfig` 部分中的 ` EnvironmentVariables` 参数。以下为 AWS CLI 命令示例，该命令创建一个使用两个环境变量（具有 `Environment` 和 `Region` 密钥）的金丝雀。

```
aws synthetics create-canary --cli-input-json '{
   "Name":"nameofCanary",
   "ExecutionRoleArn":"roleArn",
   "ArtifactS3Location":"s3://amzn-s3-demo-bucket-123456789012-us-west-2",
   "Schedule":{
      "Expression":"rate(0 minute)",
      "DurationInSeconds":604800
   },
   "Code":{
      "S3Bucket": "canarycreation",
      "S3Key": "cwsyn-mycanaryheartbeat-12345678-d1bd-1234-abcd-123456789012-12345678-6a1f-47c3-b291-123456789012.zip",
      "Handler":"pageLoadBlueprint.handler"
   },
   "RunConfig": {
      "TimeoutInSeconds":60,
      "EnvironmentVariables": {
         "Environment":"Production",
         "Region": "us-west-1"
      }
   },
   "SuccessRetentionPeriodInDays":13,
   "FailureRetentionPeriodInDays":13,
   "RuntimeVersion":"syn-nodejs-2.0"
}'
```

## 将您的金丝雀与其他 AWS 服务集成
<a name="CloudWatch_Synthetics_Canaries_AWS_integrate"></a>

所有金丝雀都可以使用 AWS SDK 库。在编写金丝雀时，您可以使用此库将金丝雀与其他 AWS 服务集成。

为此，您需要将以下代码添加到您的金丝雀中。对于这些例子，AWS Secrets Manager 用作与金丝雀集成的服务。
+ 导入 AWS SDK。

  ```
  const AWS = require('aws-sdk');
  ```
+ 为要集成的 AWS 服务创建客户端。

  ```
  const secretsManager = new AWS.SecretsManager();
  ```
+ 使用客户端对该服务进行 API 调用。

  ```
  var params = {
    SecretId: secretName
  };
  return await secretsManager.getSecretValue(params).promise();
  ```

下面的金丝雀脚本代码段更详细地演示了与 Secrets Manager 集成的示例。

```
var synthetics = require('@aws/synthetics-puppeteer');
const log = require('@aws/synthetics-logger');
 
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
 
const getSecrets = async (secretName) => {
    var params = {
        SecretId: secretName
    };
    return await secretsManager.getSecretValue(params).promise();
}
 
const secretsExample = async function () {
    let URL = "<URL>";
    let page = await synthetics.getPage();
    
    log.info(`Navigating to URL: ${URL}`);
    const response = await page.goto(URL, {waitUntil: 'domcontentloaded', timeout: 30000});
    
    // Fetch secrets
    let secrets = await getSecrets("secretname")
   
    /**
    * Use secrets to login. 
    *
    * Assuming secrets are stored in a JSON format like:
    * {
    *   "username": "<USERNAME>",
    *   "password": "<PASSWORD>"
    * }
    **/
    let secretsObj = JSON.parse(secrets.SecretString);
    await synthetics.executeStep('login', async function () {
        await page.type(">USERNAME-INPUT-SELECTOR<", secretsObj.username);
        await page.type(">PASSWORD-INPUT-SELECTOR<", secretsObj.password);
        
        await Promise.all([
          page.waitForNavigation({ timeout: 30000 }),
          await page.click(">SUBMIT-BUTTON-SELECTOR<")
        ]);
    });
   
    // Verify login was successful
    await synthetics.executeStep('verify', async function () {
        await page.waitForXPath(">SELECTOR<", { timeout: 30000 });
    });
};

exports.handler = async () => {
    return await secretsExample();
};
```

## 强制金丝雀使用静态 IP 地址
<a name="CloudWatch_Synthetics_Canaries_staticIP"></a>

您可以设置金丝雀，使其使用静态 IP 地址。

**强制金丝雀使用静态 IP 地址**

1. 创建新的 VPC。有关更多信息，请参阅[在您的 VPC 中使用 DNS](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html)。

1. 创建新的互联网网关。有关更多信息，请参阅[在您的 VPC 中添加互联网网关](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html#working-with-igw)。

1. 在新的 VPC 内创建公有子网。

1. 向 VPC 添加新的路由表。

1. 在新路由表中添加一条从 `0.0.0.0/0` 到互联网网关的路由。

1. 将新的路由表与公有子网关联。

1. 创建弹性 IP 地址。有关更多信息，请参阅[弹性 IP 地址](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html)。

1. 创建一个新的 NAT 网关，并将其分配给公有子网和弹性 IP 地址。

1. 在 VPC 中创建私有子网。

1. 向 VPC 默认路由表中添加一条从 `0.0.0.0/0` 到 NAT 网关的路由

1. 创建金丝雀。