基础设施即代码:Terraform和AWS无服务器

基础设施即代码

基础设施即代码(IaC)是一种通过机器可读的定义文件管理设备和服务器的方法。从根本上说,你要写下你希望基础设施是什么样子,以及应该在该基础设施上运行什么代码。然后,按下一个按钮,说“部署我的基础设施”即可。BAM是你已经准备投入使用的应用程序,它运行在服务器上,后台是一个通过API访问的数据库。你刚刚用IaC定义了所有的基础设施。

IaC是DEVOPS团队的一项重要实践,已经集成到CI/CD管道。

Terraform是HashiCorp推出的一个很棒的基础设施即代码工具(https://www.terraform.io/)。

我个人使用它来提供和维护AWS上的基础设施。我在这方面有很好的经验。

基础设施即代码:Terraform和AWS无服务器_第1张图片

介绍和演示

我将通过一个例子来演示IaC。我们将在AWS上创建一个应用程序。我在GitLab上提供了代码:https://gitlab.com/nxtra/codingtips-blog。用户可以输入编码提示并查看其他用户输入的所有编码提示。这些提示存储在一个NoSQL数据库AWS DynamoDB中。存储和检索这些提示是由Lambda函数完成的,这些函数从数据库中获取或存放提示。要使用这个应用程序,用户必须能够调用这些Lambda函数。因此,我们通过AWS API网关公开Lambda函数。以下是该应用程序的总体架构。

基础设施即代码:Terraform和AWS无服务器_第2张图片

你可以将这些函数组合到一个Web页面上,用户可以在该页面中输入提示并查看已有的所有提示。下面是最终结果:

基础设施即代码:Terraform和AWS无服务器_第3张图片

让我们深入了解一下。

创建应用程序

现在,我将介绍创建你在上面的演示中所看到的应用程序的步骤。IaC是重点。我将展示必要的代码和AWS CLI命令,但我不会详细解释它们,因为这不是本文的目的。我将重点介绍Terraform的定义。欢迎复制我在下文中提供的存储库,并按照本文提供的步骤进行操作。

准备工作

  • 安装Terraform
  • 安装AWS CLI
  • 在GitLab上检出库:https://gitlab.com/nxtra/codingtips-blog
  • 准备好迎接IaC带来的惊喜

Terraform:基础

你将使用Terraform配置的主要内容是资源。资源是应用程序基础设施的组件。例如,一个Lambda函数、一个API网关部署、一个DynamoDB数据库……资源是通过在关键字resource后面加上类型和名称来定义的。名称可以任意选择。类型是固定的。例如:resource “aws_dynamodb_table” “codingtips-dynamodb-table”。

要按照这篇文章的介绍进行操作,你需要知道以下两个基本的Terraform命令。

terraform apply

Terraform apply将开始准备你定义的所有基础设施,创建你的数据库,设置Lambda函数,准备API网关。

terraform destroy

Terraform destroy将删除你在云中配置的所有基础设施。如果你正确地使用了Terraform,应该就用不到这个命令。但是,如果你想从头重新开始,则可以使用此命令删除所有现有的基础设施。不用担心,你的机器上仍然有所有基础设施的完整描述,因为你使用的是基础设施即代码。

我们将把所有使用Terraform定义的基础设施放在同一个文件夹中。文件需要使用.tf扩展名。

General

让我们从创建一个general.tf文件开始。

provider \u0026quot;aws\u0026quot; {  region = \u0026quot;eu-west-1\u0026quot;}# 变量variable \u0026quot;lambda_version\u0026quot;     { default = \u0026quot;1.0.0\u0026quot;}variable \u0026quot;s3_bucket\u0026quot;          { default = \u0026quot;codingtips-node-bucket\u0026quot;}

Provider块指明我们正在向AWS上部署。你还可以在这里提供将用于部署的凭证。如果你已经在自己的机器上正确地设置了AWS CLI,那么,.aws文件夹中就会有默认凭证。如果没有指定凭证,Terraform将使用这些默认凭证。

变量有一个名称,我们可以在Terraform配置的任何地方引用它。例如,我们可以使用${var.s3_bucket)引用s3_bucket变量。当你在多个地方使用相同的变量时,这非常方便。在这篇文章中,我不会使用太多的变量,因为这会使你的Terraform配置添加更多的引用,我希望它尽可能清晰。

数据库:DynamoDB

我们从基础开始。我们所有的编码提示将存储在哪里?没错,在数据库中。这个数据库是我们的基础设施的一部分,将在一个名为dynamo.tf的文件中定义。

resource \u0026quot;aws_dynamodb_table\u0026quot; \u0026quot;codingtips-dynamodb-table\u0026quot; {  name = \u0026quot;CodingTips\u0026quot;  read_capacity = 5  write_capacity = 5  hash_key = \u0026quot;Author\u0026quot;  range_key = \u0026quot;Date\u0026quot;  attribute = [    {      name = \u0026quot;Author\u0026quot;      type = \u0026quot;S\u0026quot;    },    {      name = \u0026quot;Date\u0026quot;      type = \u0026quot;N\u0026quot;    }]}

因为Dynamo是一个NoSQL数据库,我们不需要预先指定所有属性。我们唯一需要提供的是AWS用来构建分区键的元素。当你提供一个哈希键和一个排序键时,AWS会把它们组合成一个惟一的分区键。注意“UNIQUE”这个词。确保这个组合是唯一的。

DynamoDB使用分区键值作为内部哈希函数的输入。哈希函数的输出决定了项存储在哪个分区(DynamoDB的内部物理存储)。具有相同分区键值的所有项存储在一起,按键值排序。——来自AWS文档:DynamoDB核心组件。

从dynamo.tf中的属性定义可以很容易地看出,Author (S)是一个字符串,Date (N)应该是一个数字。

IAM

在指定Lambda函数之前,我们必须为要使用的函数创建权限。这确保我们的函数具有访问其他资源(如DynamoDB)的权限。简单来说,AWS权限模型的工作原理如下:

  • 提供一个带角色的资源
  • 给该角色添加权限
  • 以下权限允许该角色访问其他资源:
    • 触发另一个资源(如Lambda函数将日志转发到CloudWatch)的权限
    • 被另一个资源(如Lambda函数被API网关触发)触发的权限
# ROLES# IAM角色规定Lambda函数可以访问的其他AWS服务resource \u0026quot;aws_iam_role\u0026quot; \u0026quot;lambda-iam-role\u0026quot; {  name = \u0026quot;codingtips_lambda_role\u0026quot;  assume_role_policy = \u0026lt;\u0026lt;EOF{  \u0026quot;Version\u0026quot;: \u0026quot;2012-10-17\u0026quot;,  \u0026quot;Statement\u0026quot;: [    {      \u0026quot;Action\u0026quot;: \u0026quot;sts:AssumeRole\u0026quot;,      \u0026quot;Principal\u0026quot;: {        \u0026quot;Service\u0026quot;: \u0026quot;lambda.amazonaws.com\u0026quot;      },      \u0026quot;Effect\u0026quot;: \u0026quot;Allow\u0026quot;,      \u0026quot;Sid\u0026quot;: \u0026quot;\u0026quot;    }  ]}EOF}# POLICIESresource \u0026quot;aws_iam_role_policy\u0026quot; \u0026quot;dynamodb-lambda-policy\u0026quot;{  name = \u0026quot;dynamodb_lambda_policy\u0026quot;  role = \u0026quot;${aws_iam_role.lambda-iam-role.id}\u0026quot;  policy = \u0026lt;\u0026lt;EOF{  \u0026quot;Version\u0026quot;: \u0026quot;2012-10-17\u0026quot;,  \u0026quot;Statement\u0026quot;: [    {      \u0026quot;Effect\u0026quot;: \u0026quot;Allow\u0026quot;,      \u0026quot;Action\u0026quot;: [        \u0026quot;dynamodb:*\u0026quot;      ],      \u0026quot;Resource\u0026quot;: \u0026quot;${aws_dynamodb_table.codingtips-dynamodb-table.arn}\u0026quot;    }  ]}EOF}

在上面的例子中,定义的第一个资源是aws_iam_role。我们稍后会把这个角色赋予Lambda函数。

然后,我们创建了资源aws_iam_role_policy,将其关联到到角色aws_iam_role。第一个aws_iam_role_policy允许这个角色调用指定DynamoDB资源上的任何操作。第二个role_policy允许具有此角色的资源向CloudWatch发送日志。

以下是几个注意事项:

  • aws_iam_role和aws_iam_role_policy通过role_policy资源的role参数关联;
  • 在aws_iam_role_policy 的Statement属性里,我们赋予(Effect属性)在特定资源上(Resource属性)执行某些操作的权限(Action属性);
  • 资源通过ARNAmazon Resource Name引用,这是资源在AWS上的唯一标识;
  • 有两种方式可以指定aws_iam_role_policy:
    • 使用until EOF语法(正如我此处的做法)
    • 使用一个单独的Terraform aws_iam_policy_document元素,然后耦合到aws_iam_role_policy
  • dynamodb-lambda-policy允许在指定的DynamoDB资源上执行所有操作,因为Action属性的设置为dynamodb:* 。你可以像下面这样进行更严格的限制,并指出可以执行的操作:
\u0026quot;dynamodb:Scan\u0026quot;, \u0026quot;dynamodb:BatchWriteItem\u0026quot;,\u0026quot;dynamodb:PutItem\u0026quot;

Lambda函数

这个应用程序有两个Lambda函数。第一个Lambda用于从数据库中获取或检索编码提示,引用名是getLambda。第二个Lambda用于将编码提示发布或发送到数据库,引用名是postlambda。

我不打算在这里复制粘贴Lambda函数的代码。你可以通过本文提供的存储库链接查看(GitLab存储库:https://gitlab.com/nxtra/codingtips-blog)。

这里,我将以getLambda函数为例。postLambda以相同的方式部署,你可以在Git存储库中找到Terraform定义。Lambda函数与我们在这里定义的其他基础设施稍有不同。我们不仅需要一个Lambda函数作为基础设施。我们还需要指定在Lambda函数中运行的代码。但是,在部署Lambda函数时,AWS将在哪里找到特定的代码呢?他们无法访问你的本地机器,是吗?这就是为什么你需要首先将代码发送到AWS上的S3桶,以便在部署函数时可以在那里找到代码。

这就意味着要创建一个S3桶,当你想在eu-west-1(爱尔兰)地区创建时,可以使用下面的命令:

aws s3api create-bucket --bucket codingtips-node-bucket --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1

现在,你需要使用zip压缩你的Lambda函数代码:

zip -r getLambda.zip index.js

并把那个文件上传到S3:

aws s3 cp getLambda.zip s3://codingtips-node-bucket/v1.0.0/getLambda.zip

注意,我将把它发送到存储桶codingtips-node-bucket的文件夹v1.0.0下,文件名为getLambda.zip。

好了,代码已经到了该到的地方。现在让我们看看如何使用Terraform来指定这些函数。

resource \u0026quot;aws_lambda_function\u0026quot; \u0026quot;get-tips-lambda\u0026quot; {  function_name = \u0026quot;codingTips-get\u0026quot;  # 桶名和之前创建的一样,为\u0026quot;aws s3api create-bucket\u0026quot;  s3_bucket = \u0026quot;${var.s3_bucket}\u0026quot;  s3_key = \u0026quot;v${var.lambda_version}/getLambda.zip\u0026quot;  # \u0026quot;main\u0026quot;是zip文件中的文件名(index.js),\u0026quot;handler\u0026quot;是属性名,其值是那个文件  # 输出的handler函数  handler = \u0026quot;index.handler\u0026quot;  runtime = \u0026quot;nodejs8.10\u0026quot;  memory_size = 128  role = \u0026quot;${aws_iam_role.lambda-iam-role.arn}\u0026quot;}resource \u0026quot;aws_lambda_permission\u0026quot; \u0026quot;api-gateway-invoke-get-lambda\u0026quot; {  statement_id  = \u0026quot;AllowAPIGatewayInvoke\u0026quot;  action        = \u0026quot;lambda:InvokeFunction\u0026quot;  function_name = \u0026quot;${aws_lambda_function.get-tips-lambda.arn}\u0026quot;  principal     = \u0026quot;apigateway.amazonaws.com\u0026quot;  # /*/*部分允许从指定API网关中的任何资源上的任何方法访问  source_arn = \u0026quot;${aws_api_gateway_deployment.codingtips-api-gateway-deployment.execution_arn}/*/*\u0026quot;}
  • 注意,我们把S3桶和查找代码的目录告知Terraform;
  • 我们为这个Lambda函数指定运行时和内存;
  • index.handler指向该文件和进入代码的函数;
  • aws_lambda_permission资源是权限,说明该Lambda函数可以被我们创建的API网关调用。

API网关

我把最难的内容留到最后一部分介绍。另一方面,它也是最有趣的。我给Terraform提供API的Swagger定义。你也可以不使用Swagger,但是,你必须指定更多的资源。

Swagger API定义如下:

swagger: '2.0'info:  version: '1.0'  title: \u0026quot;CodingTips\u0026quot;schemes:  - httpspaths:  \u0026quot;/api\u0026quot;:    get:      description: \u0026quot;Get coding tips\u0026quot;      produces:        - application/json      responses:        200:          description: \u0026quot;The codingtips request successful.\u0026quot;          schema:            type: array            items:              $ref: \u0026quot;#/definitions/CodingTip\u0026quot;      x-amazon-apigateway-integration:        uri: ${get_lambda_arn}        passthroughBehavior: \u0026quot;when_no_match\u0026quot;        httpMethod: \u0026quot;POST\u0026quot;        type: \u0026quot;aws_proxy\u0026quot;    post:      description: \u0026quot;post a coding tip\u0026quot;      consumes:        - application/json      responses:        200:          description: \u0026quot;The codingtip was added successfully\u0026quot;      x-amazon-apigateway-integration:        uri: ${post_lambda_arn}        passthroughBehavior: \u0026quot;when_no_match\u0026quot;        httpMethod: \u0026quot;POST\u0026quot;        type: \u0026quot;aws_proxy\u0026quot;definitions:  CodingTip:    type: object    description: \u0026quot;A coding tip\u0026quot;    properties:      tip:        type: string        description: \u0026quot;The coding tip\u0026quot;      date:        type: number        description: \u0026quot;date in millis when tip was entered\u0026quot;      author:        type: string        description: \u0026quot;Author of the coding tip\u0026quot;      category:        type: string        description: \u0026quot;category of the coding tip\u0026quot;    required:      - tip

如果你还不知道Swagger,复制上述代码并粘贴到在线编辑器里(Swagger编辑器)。

这将使你对API定义有一个直观的了解。

基础设施即代码:Terraform和AWS无服务器_第4张图片

在上述Swagger规范中只有一个是AWS特定的东西,那就是x-amazon-apigateway-integration。这指定了API与后端集成的细节。

  • 注意,这里永远是POST,即使资源路径的HTTP方式是GET;
  • aws_proxy表示不对请求做任何操作就传递给Lambda函数;
  • 当没有为Content-Type指定requestTemplate时,when_no_match会将请求体直接传到后台,而不做转换;
  • uri引用一个变量,如${get_lambda_arn},Terraform会将其传递给Swagger定义。我们一会就会看到。

前面已经提到,使用Swagger定义API网关有一些优点:

  • 使Terraform更为简明;
  • 你可以通过Swagger获得API的良好表示。
resource \u0026quot;aws_api_gateway_rest_api\u0026quot; \u0026quot;codingtips-api-gateway\u0026quot; {  name        = \u0026quot;CodingTipsAPI\u0026quot;  description = \u0026quot;API to access codingtips application\u0026quot;  body        = \u0026quot;${data.template_file.codingtips_api_swagger.rendered}\u0026quot;}data \u0026quot;template_file\u0026quot; codingtips_api_swagger{  template = \u0026quot;${file(\u0026quot;swagger.yaml\u0026quot;)}\u0026quot;  vars {    get_lambda_arn = \u0026quot;${aws_lambda_function.get-tips-lambda.invoke_arn}\u0026quot;    post_lambda_arn = \u0026quot;${aws_lambda_function.post-tips-lambda.invoke_arn}\u0026quot;  }}resource \u0026quot;aws_api_gateway_deployment\u0026quot; \u0026quot;codingtips-api-gateway-deployment\u0026quot; {  rest_api_id = \u0026quot;${aws_api_gateway_rest_api.codingtips-api-gateway.id}\u0026quot;  stage_name  = \u0026quot;default\u0026quot;}output \u0026quot;url\u0026quot; {  value = \u0026quot;${aws_api_gateway_deployment.codingtips-api-gateway-deployment.invoke_url}/api\u0026quot;}
  • 我们首先列出了资源aws_api_gateway_rest_api resource。该资源就像它的名称一样,提供一个API网关REST API:
    • body引用Swagger文件;
  • template_file数据源允许Terraform使用Terraform(在这里是Swagger )中未定义的信息:
    • 传递到template_file的变量用于填充文件;
  • 要使指定的REST API可用,它必须:
    • 通过资源aws_api_gateway_deployment部署;
    • 引用该REST API;
    • 它需要一个stage,这好比API的“版本”或“快照”,stage_name将用在URL中,用于调用这个API;
  • 最后,可以用来调用这个API的URL被输出到终端,并在末尾加上/api以提供正确的资源路径。

尾声

好了,我们现在来看看,这是否真得可行。这里,我在和这篇博文相关的存储库中运行terraform apply。

不错,它可以工作。我只告诉Terraform我想要的基础设施。整个安装过程自动进行!现在,你可以使用输出的URL来GET和POST编码提示。POST体应该是这样的:

{  \u0026quot;author\u0026quot;: \u0026quot;Nick\u0026quot;,  \u0026quot;tip\u0026quot;: \u0026quot;Short sessions with frequent brakes\u0026quot;,  \u0026quot;category\u0026quot;: \u0026quot;Empowerment\u0026quot;}

当你需要将API端点耦合到自己设计的前端时,你需要正确设置CORS头。如果你想迎接这个挑战,在我解决这个问题的存储库中(cors-enabled)还有另一个分支。

快乐编码人,用代码写下基础设施!

查看英文原文:INFRASTRUCTURE AS CODE: TERRAFORM AND AWS SERVERLESS

你可能感兴趣的:(基础设施即代码:Terraform和AWS无服务器)