基础设施即代码(IaC)是一种通过机器可读的定义文件管理设备和服务器的方法。从根本上说,你要写下你希望基础设施是什么样子,以及应该在该基础设施上运行什么代码。然后,按下一个按钮,说“部署我的基础设施”即可。BAM是你已经准备投入使用的应用程序,它运行在服务器上,后台是一个通过API访问的数据库。你刚刚用IaC定义了所有的基础设施。
IaC是DEVOPS团队的一项重要实践,已经集成到CI/CD管道。
Terraform是HashiCorp推出的一个很棒的基础设施即代码工具(https://www.terraform.io/)。
我个人使用它来提供和维护AWS上的基础设施。我在这方面有很好的经验。
我将通过一个例子来演示IaC。我们将在AWS上创建一个应用程序。我在GitLab上提供了代码:https://gitlab.com/nxtra/codingtips-blog。用户可以输入编码提示并查看其他用户输入的所有编码提示。这些提示存储在一个NoSQL数据库AWS DynamoDB中。存储和检索这些提示是由Lambda函数完成的,这些函数从数据库中获取或存放提示。要使用这个应用程序,用户必须能够调用这些Lambda函数。因此,我们通过AWS API网关公开Lambda函数。以下是该应用程序的总体架构。
你可以将这些函数组合到一个Web页面上,用户可以在该页面中输入提示并查看已有的所有提示。下面是最终结果:
让我们深入了解一下。
现在,我将介绍创建你在上面的演示中所看到的应用程序的步骤。IaC是重点。我将展示必要的代码和AWS CLI命令,但我不会详细解释它们,因为这不是本文的目的。我将重点介绍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.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配置添加更多的引用,我希望它尽可能清晰。
我们从基础开始。我们所有的编码提示将存储在哪里?没错,在数据库中。这个数据库是我们的基础设施的一部分,将在一个名为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)应该是一个数字。
在指定Lambda函数之前,我们必须为要使用的函数创建权限。这确保我们的函数具有访问其他资源(如DynamoDB)的权限。简单来说,AWS权限模型的工作原理如下:
# 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发送日志。
以下是几个注意事项:
\u0026quot;dynamodb:Scan\u0026quot;, \u0026quot;dynamodb:BatchWriteItem\u0026quot;,\u0026quot;dynamodb:PutItem\u0026quot;
这个应用程序有两个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;}
我把最难的内容留到最后一部分介绍。另一方面,它也是最有趣的。我给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定义有一个直观的了解。
在上述Swagger规范中只有一个是AWS特定的东西,那就是x-amazon-apigateway-integration。这指定了API与后端集成的细节。
前面已经提到,使用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;}
好了,我们现在来看看,这是否真得可行。这里,我在和这篇博文相关的存储库中运行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