如何使用AWS CDK用Lambda授权器自动部署REST APIs

406 阅读6分钟

本教程包括:

  1. 创建一个新的AWS CDK应用程序
  2. 添加一个Lambda授权器并定义CDK构造
  3. 自动化和测试CDK栈的部署

AWS云开发工具包(AWS CDK)是一个开源框架,允许您使用您选择的编程语言来定义和部署您的云资源。AWS CDK是一个基础设施即代码(IaC)解决方案,类似于Terraform,让你使用面向对象的编程语言的表达能力来定义你的云资源。AWS CDK框架为所有主要的AWS服务提供了大多数流行编程语言的库。你可以使用这些库来为你的整个系统轻松地定义一个云应用栈。它消除了上下文切换,有助于加速开发过程。开发人员不需要学习新的编程语言或新的工具就可以从AWS CDK中受益。

在本教程中,我将指导你使用AWS CDK,用基于AWS Lambda的授权器部署REST API。你将学习如何通过添加授权者、使用计划、节流、速率限制等,利用API网关结构来定制API的行为。

前提条件

对于本教程,你需要在你的机器上设置NodeJS,因为你将使用它来定义AWS CDK应用程序和AWS Lambda处理程序。你还需要在你的系统上安装AWS CLI和AWS CDK CLI,这样你就可以配置AWS凭证并手动构建你的CDK应用程序。你将需要一个AWS账户来部署应用程序,以及一个CircleCI账户来自动部署。以下是您需要的所有东西的清单,以便跟随本教程。

创建一个新的AWS CDK项目

为CDK项目创建一个新的目录,并导航到其中。运行这些命令。

mkdir aws-cdk-api-auth-lambda-circle-ci
cd aws-cdk-api-auth-lambda-circle-ci

使用CDK CLI,运行cdk init 命令,在TypeScript中创建一个新的CDK项目。

cdk init app --language typescript

这个命令创建一个新的CDK项目,有一个单一的堆栈和一个单一的应用程序。

注意。 AWS CDK支持所有主要的编程语言,包括TypeScript、Python、Java和C#。如果你选择了不同的编程语言,你仍然可以遵循本教程的步骤,但语法将根据你选择的编程语言而改变。

添加一个NodeJS Lambda函数

在本节中,你将使用NodeJS定义一个AWS Lambda函数,该函数可用于与AWS API Gateway进行代理集成。API Gateway的AWS Lambda代理集成提供了一个简单而强大的机制来构建API的业务逻辑。代理集成允许客户在通过API Gateway调用REST API时在后端调用一个AWS Lambda函数。

只要改变AWS Lambda代理集成的请求和响应对象即可。

首先,在CDK项目的根部创建一个lambda 目录。在lambda 文件夹内,创建另一个名为processJob 的文件夹。在processJob 目录中创建一个package.json 文件,用于定义依赖关系。打开package.json 文件,并添加以下内容。

{
  "name": "circle-ci-upload-csv-lambda-function",
  "version": "0.1.0",
  "dependencies": {
    "csv-stringify": "^6.0.5",
    "fs": "0.0.1-security",
    "uuid": "^8.3.2"
  }
}

这个脚本定义了项目的名称,并添加了一些将被Lambda处理器使用的依赖项。

现在,从终端导航到processJob文件夹,安装NPM包。

cd lambda/processJob
npm install

接下来,在processJob 目录中创建一个index.js 文件,并在其中添加以下代码片断。我们从链接的教程中提取了整个代码片断,只是修改了请求和响应对象。

"use strict";

const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
var fs = require('fs');
const { stringify } = require('csv-stringify/sync');
AWS.config.update({ region: 'us-west-2' });

var ddb = new AWS.DynamoDB();
const s3 = new AWS.S3();

const TABLE_NAME = process.env.TABLE_NAME;
const BUCKET_NAME = process.env.BUCKET_NAME;

exports.handler = async function (event) {
  try {
    const uploadedObjectKey = generateDataAndUploadToS3();
    const eventBody = JSON.parse(event["body"]);
    const jobId = eventBody["jobId"];
    console.log("event", jobId);
    var params = {
      TableName: TABLE_NAME,
      Item: {
        jobId: { S: jobId },
        reportFileName: { S: uploadedObjectKey },
      },
    };

    // Call DynamoDB to add the item to the table
    await ddb.putItem(params).promise();

    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        status: "success",
        jobId: jobId,
        objectKey: uploadedObjectKey,
      }),
    };
  } catch (error) {
    throw Error(`Error in backend: ${error}`);
  }
};

const generateDataAndUploadToS3 = () => {
  var filePath = "/tmp/test_user_data.csv";
  const objectKey = `${uuidv4()}.csv`;
  writeCsvToFileAndUpload(filePath, objectKey);
  return objectKey;
};

const uploadFile = (fileName, objectKey) => {
  // Read content from the file
  const fileContent = fs.readFileSync(fileName);

  // Setting up S3 upload parameters
  const params = {
    Bucket: BUCKET_NAME,
    Key: objectKey,
    Body: fileContent,
  };

  // Uploading files to the bucket
  s3.upload(params, function (err, data) {
    if (err) {
      throw err;
    }
    console.log(`File uploaded successfully. ${data.Location}`);
  });
  return objectKey;
};

function writeCsvToFileAndUpload(filePath, objectKey) {
    var data = getCsvData();
    var output = stringify(data);

    fs.writeFileSync(filePath, output);
    // we will add the uploadFile method later
    uploadFile(filePath, objectKey);
}

function getCsvData() {
    return [
      ['1', '2', '3', '4'],
      ['a', 'b', 'c', 'd']
    ];
}

请求和响应对象需要被修改是有原因的。当Lambda函数通过API网关被调用时,请求对象由JSON组成,包括请求体、HTTP方法类型、REST API资源路径、查询参数、头信息和请求上下文。下面是一个显示这一点的片段。

{
  "body": "{\"jobId\": \"1\"}",
  "path": "/path/to/resource",
  "httpMethod": "POST",
  "isBase64Encoded": false,
  "queryStringParameters": {
    ...
  },
  "headers": {
    ...
  },
  "requestContext": {
    ...
  }
}

JSON请求的有效载荷被字符串化并设置在body 参数下。要在Lambda中提取有效载荷,你必须修改代码,如下所示。

const eventBody = JSON.parse(event["body"]);
const jobId = eventBody["jobId"];

此外,由于Lambda响应将被API网关使用,你需要将响应格式化为JSON REST API响应,包括状态代码、状态、头信息和响应主体。因此,你要将实际的响应字符串化并添加到JSON对象的body 参数下,如下所示。

{
    "statusCode": 200,
    'headers': {'Content-Type': 'application/json'},
    "body": JSON.stringify({
      "status": "success",
      "jobId": jobId,
      "objectKey": uploadedObjectKey
    })
}

添加一个Lambda授权器

接下来,定义另一个AWS Lambda函数,它将作为一个自定义授权器。每当客户端调用REST API时,API Gateway将调用这个Lambda函数,将Authorization 头部值传递给它。Lambda处理程序将验证Authorization 头中发送的令牌,如果令牌有足够的权限,将返回一个IAM策略声明。如果令牌没有被授权调用REST API,Lambda处理程序将返回一个错误响应。

首先,在CDK项目的根部创建一个lambda/authorizer 目录。在authorizer 目录内添加一个package.json 文件,用于定义依赖关系。在package.json 中定义项目的名称,并添加一些将被Lambda处理程序使用的依赖项。

{
  "name": "circle-ci-auth-lambda-function",
  "version": "0.1.0",
  "dependencies": {}
}

接下来,在authorizer 目录中为授权者Lambda处理程序创建一个index.js 文件,并在其中添加一个空的Lambda处理程序。

exports.handler =  function(event, context, callback) {
    
};

在你实现Lambda处理程序之前,定义一个方法,生成IAM策略声明,授予调用授权Lambda的REST API以execute-api:Invoke 的权限。

var generatePolicy = function(principalId, effect, resource) {
    var authResponse = {};
    
    authResponse.principalId = principalId;
    if (effect && resource) {
        var policyDocument = {};
        policyDocument.Version = '2012-10-17'; 
        policyDocument.Statement = [];
        var statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; 
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        policyDocument.Statement[0] = statementOne;
        authResponse.policyDocument = policyDocument;
    }
    
    return authResponse;
}

现在你已经定义了generatePolicy 函数,实现Lambda处理程序。Lambda处理程序将从event 参数中提取授权令牌,然后验证该令牌。对于一个有效的令牌,它将调用generatePolicy 方法来返回适当的IAM策略。

exports.handler = function (event, context, callback) {
  var token = event.authorizationToken;
  switch (token) {
    case "allow":
      callback(null, generatePolicy("user", "Allow", event.methodArn));
      break;
    case "deny":
      callback(null, generatePolicy("user", "Deny", event.methodArn));
      break;
    case "unauthorized":
      callback("Unauthorized"); // Return a 401 Unauthorized response
      break;
    default:
      callback("Error: Invalid token"); // Return a 500 Invalid token response
  }
};

现在你已经定义了处理任务的Lambda以及授权的Lambda,你可以为应用程序定义CDK结构体。

为应用程序定义CDK构造

AWS CDK结构体封装了多个AWS服务的配置细节和胶合逻辑。CDK提供大多数主要编程语言的

lib/aws-cdk-api-auth-lambda-circle-ci-stack.ts 文件的内容替换为以下代码段,该代码段定义了AWS S3桶、AWS Lambda函数和AWS DynamoDB的构造。

import {
  Stack,
  StackProps,
  aws_s3 as s3,
  aws_dynamodb as dynamodb,
  aws_lambda as lambda,
  Duration
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class AwsCdkApiAuthLambdaCircleCiStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // we will add all the constructs here
    // replace bucket name with a unique name
    const circleCiGwpBucket = new s3.Bucket(this, "CircleCIGwpAuthExampleBucket", {
      bucketName: "<YOUR_BUCKET_NAME>",
    });

    const circleCiGwpTable = new dynamodb.Table(this, "CircleCIGwpAuthExampleTable", {
      tableName: "CircleCIGwpAuthExampleTable",
      partitionKey: { name: "jobId", type: dynamodb.AttributeType.STRING },
    });

    const circleCiGwpLambda = new lambda.Function(
      this,
      "CircleCiGwpProcessJobLambda",
      {
        runtime: lambda.Runtime.NODEJS_14_X,
        handler: "index.handler",
        timeout: Duration.seconds(30),
        code: lambda.Code.fromAsset("lambda/processJob/"),
        environment: {
          TABLE_NAME: circleCiGwpTable.tableName,
          BUCKET_NAME: circleCiGwpBucket.bucketName
        },
      }
    );

    circleCiGwpBucket.grantPut(circleCiGwpLambda);
    circleCiGwpTable.grantReadWriteData(circleCiGwpLambda);
  }
}

这些是基本的CDK构造,可以创建一个新的S3桶,一个新的DynamoDB表,以及一个使用lambda/processJob 目录中定义的Lambda处理程序的Lambda函数。定义完构造后,给Lambda函数授予适当的IAM权限。

AWS S3桶的名称在所有AWS账户中都是唯一的,所以你需要为你的桶提供一个唯一的名称。

  • TABLE_NAME 和 作为 变量传递,它们将可以在AWS Lambda处理程序中使用。BUCKET_NAME environment

在定义更多的构造之前,你需要在栈中定义。

  • CDK构架来定义授权Lambda。
  • API GatewayTokenAuthorizer 构造,使用授权Lambda作为其处理程序。
  • API GatewayRestApi 构造,用于服务。
  • API GatewayLambdaIntegration 构造,使用进程作业Lambda 作为其处理程序。

使用Lambda集成结构,为API令牌授权者资源设置添加授权处理方法。

定义一个授权Lambda

接下来,添加一个CDK构造来创建一个AWS Lambda函数,用于自定义授权。授权Lambda将使用NodeJS运行时间和你在lambda/authorizer 目录中定义的代码。

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  const circleCiAuthLambda = new lambda.Function(
    this,
    "CircleCiAuthLambda",
    {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: "index.handler",
      timeout: Duration.seconds(30),
      code: lambda.Code.fromAsset("lambda/authorizer/"),
    }
  );
}

定义一个令牌授权器

要定义一个API Gateway令牌授权器,请为TokenAuthorizer 。令牌授权器使用你之前定义的授权Lambda函数。

import {
  Stack,
  StackProps,
  aws_s3 as s3,
  aws_dynamodb as dynamodb,
  aws_lambda as lambda,
  //update existing import to add aws_apigateway
  aws_apigateway as apigateway,
  Duration
} from 'aws-cdk-lib';

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  const circleCiAuthorizer = new apigateway.TokenAuthorizer(this, 'CircleCIGWPAuthorizer', {
    handler: circleCiAuthLambda
  });
}

定义REST API服务

接下来,定义一个API Gateway REST API服务,为它提供一个名称和描述。你将使用这个RestApi 服务来添加资源到它。

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  const circleCiGwpApi = new apigateway.RestApi(this, "CircleCIGWPAPI", {
    restApiName: "Circle CI GWP API",
    description: "Sample API for Circle CI GWP"
  });
}

现在你可以向circleCiGwpApi 服务添加资源。资源是你正在创建的实际端点,不包括基本URL。

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  const jobResource = circleCiGwpApi.root.addResource("jobs");
}

添加一个Lambda集成

Lambda集成将AWS Lambda函数集成到API网关方法中。你将使用你之前定义的process job Lambda函数作为Lambda集成的处理程序。

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  const processJobIntegration = new apigateway.LambdaIntegration(
    circleCiGwpLambda
  );
}

用Lambda授权器添加一个API网关方法

最后,添加一个POST 方法到jobResource ,并使用授权Lambda作为auth处理器。

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  jobResource.addMethod("POST", processJobIntegration, {
    authorizer: circleCiAuthorizer,
    authorizationType: apigateway.AuthorizationType.CUSTOM,
  });
}

定制API使用计划

在本节中,你将学习如何通过定义使用计划、节流设置和速率限制来定制REST API的行为和体验。

该计划使用API密钥来识别API客户,以及谁可以访问每个密钥的相关API阶段。

  • 使用计划。一个使用计划指定谁可以访问部署的API。使用计划可以选择性地在方法层面上设置。API密钥与使用计划相关联,用于识别每个密钥可以访问API的API客户端。
  • API密钥。API密钥是字符串值,可用于授予对你的API的访问。
  • 节流限制。节流限制决定了请求节流应该开始的阈值,它可以在API或方法层面上设置。

现在你可以为API定义一个使用计划。

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  const circleCiUsagePlan = circleCiGwpApi.addUsagePlan('CircleCiUsagePlan', {
    name: 'CircleCiEasyPlan',
    throttle: {
      rateLimit: 100,
      burstLimit: 2
    }
  });
}

注意,你在定义使用计划的同时也在定义节流限制。rateLimit 是指API在一个较长时期内的每秒平均请求数。burstLimit 是指在一到几秒钟的时间内,最大的API请求率限制。设置节流限制是可选的,你可以选择不为你的API执行任何此类限制。

因为使用计划需要一个API密钥与之相关联以识别客户,所以要给它添加一个API密钥。

constructor(scope: Construct, id: string, props?: StackProps) {
  // add this snippet below the existing code
  const circleCiApiKey = circleCiGwpApi.addApiKey('CircleCiApiKey');
  circleCiUsagePlan.addApiKey(circleCiApiKey);
}

CircleCiApiKey 与CircleCI仪表板没有任何关联。你可以根据你的要求使用任何其他的API密钥的名称。

部署CDK栈

现在你已经在堆栈中定义了CDK结构,你可以继续将应用程序部署到AWS账户中。首先,在使用CircleCI进行自动部署之前,先手动部署该应用程序。确保你的系统上安装了AWS CDK CLI。除此以外,你还需要安装AWS CLI并配置访问证书。你可以按照先决条件部分的链接来安装CLI和配置凭证。

运行以下命令来启动应用程序并部署它。

cdk bootstrap
cdk deploy

当你执行cdk deploy 命令时,它将提示你确认将应用于你的账户的IAM角色/政策变化。注意,终端将显示你部署的REST API服务的基本URL。抓住这个URL并保持它的方便性,因为你将在下一节中使用它来测试API。

测试部署的API

现在应用程序已经被部署到AWS账户,通过使用curl 调用API端点来测试API。在curl 请求中替换你在上一节中获得的基本URL。

curl -X POST \
  '<base_URL>/jobs' \
  --header 'Accept: */*' \
  --header 'Authorization: allow' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "jobId": "1"
}'

注意,你已经设置了Authorization: allow 头,作为授权Lambda验证的令牌。

在执行curl 请求时,你应该收到一个类似于下图的成功响应。

API success response

不要在Authorization 头中传递allow 的值,试着传递deny 或其他值,以确保API只有在收到有效的令牌时才会返回成功响应。

curl -X POST \
  '<baseURL>/jobs' \
  --header 'Accept: */*' \
  --header 'Authorization: deny' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "jobId": "1"
}'

正如预期的那样,由于令牌无效,API会返回一个授权响应。

API unauthorized response

使用CircleCI自动部署应用程序

现在你已经能够使用命令行手动部署CDK应用程序,自动化工作流程,以便每次推送代码到主分支时,基础设施的变化可以被自动打包和部署。为了自动部署,你需要。

  • 更新.gitignore
  • 更新NPM脚本
  • 添加配置脚本
  • 创建一个CircleCI项目
  • 设置环境变量

更新 .gitignore

cdk init 命令生成的代码包含一个.gitignore 文件,默认忽略所有.js 文件。请确保用下面的代码片段替换.gitignore 的内容。

!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out

更新NPM脚本

我们的CircleCI部署配置使用NPM脚本来执行deploy和diff命令。添加以下脚本到根级package.json 文件。

// update the aws-cdk-api-auth-lambda-circle-ci/package.json file with the following scripts
{
  ... 
  "scripts": {
    ...
    // add the ci_diff and ci_deploy scripts
    "ci_diff": "cdk diff -c env=${ENV:-stg} 2>&1 | sed -r 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g' || true",
    "ci_deploy": "cdk deploy -c env=${ENV:-stg} --require-approval never"
  },
  ...
}

添加配置脚本

首先,在项目的根部添加一个.circleci/config.yml 脚本,包含CI管道的配置文件。在config.yml 中添加以下代码片段。

version: 2.1

orbs:
  aws-cli: circleci/aws-cli@2.0.6
executors:
  default:
    docker:
      - image: "cimg/node:14.18.2"
    environment:
      AWS_REGION: "us-west-2"
jobs:
  build:
    executor: "default"
    steps:
      - aws-cli/setup:
          aws-access-key-id: AWS_ACCESS_KEY
          aws-secret-access-key: AWS_ACCESS_SECRET
          aws-region: AWS_REGION_NAME
      - checkout
      - run:
          name: 'Install Authorizer lambda packages'
          command: |
            cd lambda/processJob && npm install
      - run:
          name: 'Install Process Job lambda packages'
          command: |
            cd lambda/processJob && npm install
      - run:
          name: "build"
          command: |
            npm install
            npm run build
      - run:
          name: "cdk_diff"
          command: |
            if [ -n "$CIRCLE_PULL_REQUEST" ]; then
              export ENV=stg
              if [ "${CIRCLE_BRANCH}" == "develop" ]; then
                export ENV=prd
              fi 
              pr_number=${CIRCLE_PULL_REQUEST##*/}
              block='```'
              diff=$(echo -e "cdk diff (env=${ENV})\n${block}\n$(npm run --silent ci_diff)\n${block}")
              data=$(jq -n --arg body "$diff" '{ body: $body }') # escape
              curl -X POST -H 'Content-Type:application/json' \
                -H 'Accept: application/vnd.github.v3+json' \
                -H "Authorization: token ${GITHUB_TOKEN}" \
                -d "$data" \
                "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/issues/${pr_number}/comments"
            fi
      - run:
          name: "cdk_deploy"
          command: |
            if [ "${CIRCLE_BRANCH}" == "main" ]; then
              ENV=prd npm run ci_deploy
            elif [ "${CIRCLE_BRANCH}" == "develop" ]; then
              ENV=stg npm run ci_deploy
            fi

CI脚本使用aws-cli orb来设置AWS配置,如访问密钥和秘密。

cdk_deploy 命令检查分支,并相应地在prdstg 环境中进行部署。请注意,cdk_deploy 命令会执行package.json 文件中定义的ci_deploy 脚本。

我们的管道配置将负责构建、打包和部署CDK栈到指定的AWS账户。提交修改并推送到GitHub仓库。

为该应用程序创建一个CircleCI项目

接下来,使用CircleCI控制台将该仓库设置为CircleCI项目。在Circle CI控制台,点击项目标签,搜索GitHub仓库的名称。点击为你的项目设置项目的按钮。

Circle CI set up project

会出现一个对话提示,询问你是要手动添加一个新的配置文件还是使用一个现有的配置文件。由于你已经向代码库推送了所需的配置文件,选择最快的选项,并输入承载你的配置文件的分支的名称。点击Set Up Project继续。

Select configuration

完成设置将自动触发流水线。由于你没有定义环境变量,管道在第一次运行时会失败。

设置环境变量

在项目页面,点击项目设置,进入环境变量标签。在出现的屏幕上,点击添加环境变量按钮,添加以下环境变量。

  • AWS_ACCESS_KEY 从AWS控制台中的IAM角色页面获得的
  • AWS_ACCESS_SECRET 从AWS控制台中的IAM角色页面获得
  • AWS_REGION_NAME 到一个你想部署应用程序的区域

一旦你添加了环境变量,它应该在仪表板上显示关键值。

现在环境变量已经配置好了,再次触发管道。这一次,构建应该成功。

Circle CI pipeline builds successfully

总结

在本教程中,你看到了如何使用AWS CDK结构来轻松部署应用程序,并使用REST API暴露其功能。CDK可用于轻松插入基于Lambda的自定义授权器,并通过定义使用计划、API密钥和节流限制来进一步定制应用体验。AWS CDK允许你在IaC代码中使用熟悉的工具和编程语言。它还可以让你编写可测试的代码,并将基础设施代码与你现有的代码审查工作流程相结合。