自动将AWS Lambda函数部署到AWS CDK上

579 阅读6分钟

本教程包括。

  1. 定义你的AWS CDK应用程序和AWS Lambda处理器
  2. 手动构建和部署你的CDK应用程序
  3. 自动部署

当你构建一个基于云的应用程序时,你可以选择使用云提供商提供的GUI(图形用户界面)或CLI(命令行界面)来部署资源。这种方法在只有少量资源的情况下可以很好地工作,但随着你的应用程序的复杂性增加,手动管理基础设施会变得很困难。

你可以使用像Terraform或AWS CDK这样的解决方案,让你以编程方式管理你的基础设施代码,而不是手动部署你的云资源。借助面向对象编程语言的表现力,AWS CDK可以让你使用现有的技能和工具来开发云基础设施,加快开发过程。使用AWS CDK可以消除上下文切换的需要,因为您可以使用相同的IDE和工具同时定义基础设施代码和运行时代码。AWS CDK还使你的代码更容易与git工作流集成,并允许你使用CI/CD管道来自动化部署过程。

在本教程中,我将指导你使用AWS云开发工具包(CDK)来部署一个与AWS S3和AWS DynamoDB交互的AWS Lambda函数。

前提条件

在本教程中,你需要在你的系统中设置Node.js,以定义你的AWS CDK应用程序和AWS Lambda处理器。你还需要在你的系统上安装AWS CLI和AWS CDK CLI,以便你能够配置AWS凭证并手动构建你的CDK应用程序。你将需要一个AWS账户来部署应用程序,以及一个CircleCI账户来自动部署。下面是一份你需要的所有东西的清单,以便跟随本教程进行。

我们的教程是不分平台的,但以CircleCI为例。 如果你没有CircleCI账户。 在这里注册一个免费账户.

创建一个新的AWS CDK项目

首先,为您的CDK项目创建一个新的目录,并导航到它。

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

接下来,使用CDK CLI运行cdk init 命令,它使用TypeScript创建一个新的CDK项目。app 参数指定了初始化项目时要使用的模板。

cdk init app --language typescript

执行这个命令可以创建一个新的CDK项目,里面有一些文件。在本教程的后面,我将解释其中一些文件的意义和其中定义的结构。

注意。 CDK支持多种语言,如TypeScript、Python、Java和C#。你可以选择使用你所熟悉的任何语言。

添加一个NodeJS Lambda函数

在这一部分,你将使用Node.js定义一个AWS Lambda函数。该Lambda函数演示了如何将CSV文件保存到AWS S3,并向DynamoDB表添加条目。

为了开始,在CDK项目的根部创建一个lambda 目录。在lambda 目录中,为lambda handler添加一个index.js 文件,为定义依赖关系添加一个package.json 文件。

package.json 文件中定义NodeJS项目的名称,并添加一些将被我们的处理器使用的依赖项。以下是该文件应包含的内容。

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

在你添加了依赖项之后,在lambda 目录下运行npm install 命令来安装这些包。

接下来,在index.js 文件中,定义一个空函数。在本教程的后面,你将实现这个函数。

exports.handler = async function (event) {

}

现在你可以开始实施了。用假数据创建一个CSV文件,并将其保存为一个临时文件。将这个代码段添加到你先前创建的index.js 文件的底部。

// add imports at the top of the file
var fs = require('fs');
const {stringify} = require('csv-stringify');

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 some CSV data
    return [
      ['1', '2', '3', '4'],
      ['a', 'b', 'c', 'd']
    ];
}

接下来,在index.js 文件中定义另一个函数,该函数接收一个本地文件路径和S3对象路径,并将文件上传到AWS S3。

// add imports at the top of the file
const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-west-2' });

const s3 = new AWS.S3();
const BUCKET_NAME = process.env.BUCKET_NAME

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;
};

这个函数读取本地文件的内容并使用AWS.S3 SDK的upload 函数来上传文件。

最后,添加你在index.js 文件中创建的空AWS Lambda处理器的实现。该Lambda处理程序在其事件参数中接收jobId 。该处理程序首先将CSV文件上传到AWS S3,然后用jobId 和上传对象的S3路径更新DynamoDB表。

//add import at the top of the file
const { v4: uuidv4 } = require('uuid');

var ddb = new AWS.DynamoDB();
const TABLE_NAME = process.env.TABLE_NAME

exports.handler = async function (event) {
    try {
        const uploadedObjectKey = generateDataAndUploadToS3()
        const jobId = 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 {
            "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
}

处理程序使用AWS.DynamoDB SDK的putItem 方法,在DynamoDB表中插入一个新项目。

为应用程序定义CDK结构

现在你已经定义了AWS Lambda处理程序,你可以定义所有将在你的应用程序中使用的CDK构造。AWS CDK 构造是云组件,它封装了使用一个或多个AWS服务的配置细节和胶合逻辑。CDK为最常用的AWS服务提供了一个构造

使用app 模板生成CDK项目,会创建lib/aws-cdk-lambda-circle-ci-stack.ts 文件。该文件包含AwsCdkLambdaCircleCiStack 类。使用这个文件来定义CDK构造。

//  The snippet shows the original contents for reference. You do not need to replace the file contents.
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    // we will add all the constructs here
  }
}

接下来,回顾一下你的应用程序需要什么来工作。这些任务将在本教程的下一节中描述。

  • 创建一个AWS S3桶,用来存放由AWS Lambda函数上传的CSV文件。
  • 创建一个DynamoDB表,其中的jobId 和对象键将由AWS Lambda函数更新。
  • 定义一个AWS Lambda函数,它将同时使用S3和DymamoDB表。确保AWS Lambda函数有BUCKET_NAMETABLE_NAME 作为参数。
  • 确保AWS Lambda函数有足够的权限来执行对S3桶和DymamoDB表的操作。

定义一个AWS S3桶

要定义一个AWS S3桶,添加一个CDK结构,在lib/aws-cdk-lambda-circle-ci-stack.ts 文件中定义的constructor 内创建一个AWS S3桶。AWS S3桶的名字在所有AWS账户中都是唯一的,所以你需要为你的桶提供一个唯一的名字。

import { Stack, 
  StackProps,
  //update the existing import to add aws_s3
  aws_s3 as s3
 } from 'aws-cdk-lib';

constructor(scope: Construct, id: string, props?: StackProps) {
  super(scope, id, props);

  // we will add all the constructs here
  // provide a unique name for your S3 bucket
  const circleCiGwpBucket = new s3.Bucket(this, "circle-ci-gwp-bucket", {
    bucketName: "<YOUR_BUCKET_NAME>",
  });
}

在这一点上,如果你尝试部署堆栈,它将简单地部署一个CloudFormation应用程序并为你创建一个AWS S3桶。

定义一个DynamoDB表

要定义一个DynamoDB表,添加一个CDK结构,在lib/aws-cdk-lambda-circle-ci-stack.ts 文件中定义的constructor 内创建一个DynamoDB表。

import {
  Stack,
  StackProps,
  aws_s3 as s3,
  //update the existing import to add aws_dynamodb
  aws_dynamodb as dynamodb
} from 'aws-cdk-lib';

constructor(scope: Construct, id: string, props?: StackProps) {
  // other code //
  //add the following construct after the existing code in the constructor
  const circleCiGwpTable = new dynamodb.Table(this, "CircleCIGwpTable", {
    tableName: "CircleCIGwpTable",
    partitionKey: { name: "jobId", type: dynamodb.AttributeType.STRING },
  });
}

该表的名称在你的AWS账户中必须是唯一的。如果你在AWS账户中已经有一个名为CircleCIGwpTable 的表,请在定义DynamoDB构造的同时更新tableName

注意,你已经将jobId 定义为该表的主分区键。这将确保jobId 的值在表中是唯一的。

定义一个AWS Lambda函数

要定义一个AWS Lambda函数,添加一个CDK构造,在lib/aws-cdk-lambda-circle-ci-stack.ts 文件中定义的constructor 内创建一个AWS Lambda函数。AWS Lambda函数将使用NodeJS运行时间,并将使用我们在lambda 目录中定义的代码。另外,S3桶名和DynamoDB表名将作为环境变量传递给该函数。

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

constructor(scope: Construct, id: string, props?: StackProps) {
  // other code //
  //add the following construct after the existing code in the constructor
  const circleCiGwpLambda = new lambda.Function(
    this,
    "CircleCiGwpLambda",
    {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: "index.handler",
      timeout: Duration.seconds(30),
      code: lambda.Code.fromAsset("lambda/"),
      environment: {
        TABLE_NAME: circleCiGwpTable.tableName,
        BUCKET_NAME: circleCiGwpBucket.bucketName
      },
    }
  );
}

这个代码片段指定AWS Lambda函数的超时时间为30秒。Lambda函数的最大执行时间可以达到15分钟。

给AWS Lambda函数授予权限

最后,给AWS Lambda函数授予足够的权限。Lambda函数需要S3对象的putObject 权限。通过在lib/aws-cdk-lambda-circle-ci-stack.ts 的构造函数中的现有代码后添加以下构造来授予这些权限。

circleCiGwpBucket.grantPut(circleCiGwpLambda);

Lambda函数还需要DynamoDB表的读/写权限。在lib/aws-cdk-lambda-circle-ci-stack.ts 的构造函数中的现有代码后添加以下构造。

circleCiGwpTable.grantReadWriteData(circleCiGwpLambda);

你可以使用AWS CDK IAM模块定义更复杂的IAM策略。

部署CDK栈

现在你已经在你的堆栈中定义了CDK构造,你可以将应用程序部署到AWS账户中。首先手动部署该项目,以确保一切正常。然后,一旦你验证它的功能,你可以使用CircleCI自动部署。

在第一次部署项目之前,你需要使用cdk CLI来引导项目。引导应用程序提供AWS CDK可能需要的资源,以部署您的应用程序。这些资源可能包括一个用于存储部署相关文件的S3桶和用于授予部署权限的IAM角色。从项目的根部发出这个命令。

cdk bootstrap

确保你的系统上配置了AWS凭证。如果配置了凭证,CDK将自动使用它。

接下来,将应用程序部署到AWS账户中。

cdk deploy

一旦你执行了该命令,你可能会被提示确认将应用于你的账户的IAM角色/政策变化。如果你的应用程序设置正确,而且你的系统上有所有的先决条件,那么部署工作应该成功。

使用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脚本来执行deploydiff 命令。将这些脚本添加到根级别的package.json 文件中。

// update the aws-cdk-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管道的配置文件。将这个代码片段添加到其中。

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_lambda_packages'
          command: |
            cd lambda/authorizer && npm install
            cd ../../
            cd lambda/processJob && npm install
            cd ../../
      - 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脚本使用CircleCI的aws-cli orb来设置AWS配置--访问密钥和秘密。build 工作有几个不同的步骤,安装软件包,计算diff ,并部署变化。cdk_diff 步骤只在拉动请求上执行,并在PR上添加一个评论,总结基础设施的变化。

cdk_deploy 命令检查分支并只在prdstg 环境下部署。cdk_deploy 命令执行package.json 文件中定义的ci_deploy 脚本。

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

注意。 如果您的地区不同,请务必用您自己的地区替换AWS_REGION

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

接下来,使用CircleCI控制台将软件库设置为一个CircleCI项目。在控制台的项目标签上,搜索GitHub repo名称。点击为你的项目设置项目

Circle CI set up project

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

Circle CI project configuration

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

设置环境变量

在项目仪表板上点击项目设置,然后点击环境变量标签。点击添加环境变量。你应该已经创建了AWS访问密钥和秘密,正如本教程的前提条件中提到的。将这些值分别作为AWS_ACCESS_KEYAWS_ACCESS_SECRET 。此外,将AWS_REGION_NAME 的环境变量设置为你希望部署应用程序的区域。

Circle CI set up environment variables

一旦环境变量被配置好,再次运行管道。这一次,它应该成功构建。

Circle CI pipeline builds successfully

总结

本教程向你展示了AWS CDK如何使管理基础设施相关的代码变得更容易。通过AWS CDK,你可以使用你熟悉的语言为你的应用程序提供资源。AWS CDK允许你在定义你的应用程序时使用逻辑语句和面向对象技术。另外,AWS CDK使基础设施的代码可以使用行业标准协议进行测试。

本教程带你了解了一个非常常见的用例,即定义一个具有与其他AWS服务互动的包依赖关系的AWS Lambda函数。我希望你同意,用所有的AWS服务定义堆栈,并使用这里列出的步骤为其授予细粒度的权限是多么简单。你可以在GitHub上查看本教程中使用的全部源代码。如果你想定义一个类似的堆栈,GitHub项目也可以作为你的模板来使用。