terraform 如何创建数据盘(DevOps系列Terraform)

每天三分钟,知识更轻松。 欢迎关注同名微信公众账号极客24h。

terraform 如何创建数据盘(DevOps系列Terraform)(1)

本文是深入理解Terraform系列第二部分,在第一部分中。我们解释为什么我们选择Terraform 作为我们的IAC工具。那么,在本文中,我们会进入实战基础部分——首先会了解下Terraform的基础,以及如何使用Terraform 来定义和管理你的基础设施。

官方的学习资料已经对Terraform的各个部分(resource,输入/输出变量等)做了很好的讲解。所以在本文,我们将集中讨论如何将这些元素组合在一起来创建一个生产环境的例子。特别是,我们将在集群中的AWS上配置多个服务器,并部署负载均衡器以在该集群中分配负载。此示例中创建的基础设施是运行可扩展,高可用性Web服务和微服务的基本起点。

本指南面向AWS和Terraform新手,所以如果您以前没有使用过任何一个,请不要担心。我们将逐步指导您完成整个过程:

  1. 创建你的AWS账户
  2. 安装 Terraform
  3. 部署单台服务器
  4. 部署单台Web服务器
  5. 部署一台可配置的Web服务器
  6. 部署一组Web服务器集群
  7. 部署负载均衡器
  8. 清理资源

你可以在以下Github 中找到案例代码:

https://github.com/yangvipguang/intro-to-terraform

Note:案例所使用的Terraform基于最新的:Terraform 0.12.x

创建AWS账户

Terraform可以为许多不同类型的云提供商提供基础设施配置,包括AWS,Azure,Google Cloud,DigitalOcean等等。在本教程中,我们选择了Amazon Web Services(AWS),因为:

  • 它提供了大量可靠且可扩展的云托管服务,包括弹性计算云(EC2),Auto Scaling组(ASG)和Elastic Load Balancing(ELB)。如果您发现AWS术语令人困惑,请务必以纯英语查看AWS。
  • 到目前为止,AWS是最受欢迎的云基础架构提供商。
  • AWS提供了一个丰富的免费套餐,允许您免费运行所有这些示例。

首次注册AWS时,您最初以root用户身份登录。此用户帐户具有对所有内容的访问权限,因此从安全角度来看,我们建议仅使用它来创建权限更有限的其他用户帐户(请参阅IAM最佳实践)。要创建更有限的用户帐户,请转到身份和访问管理(IAM)控制台,单击“用户”,然后单击蓝色的“创建新用户”按钮。输入用户的名称,并确保选中“为每个用户生成访问密钥”:

terraform 如何创建数据盘(DevOps系列Terraform)(2)

单击“创建”按钮,您将能够看到该用户的安全凭证,其中包括访问密钥ID和秘密访问密钥。你必须立即保存这些,因为之后它们永远不会再显示。我们建议将它们存储在安全的地方(例如密钥管理器,如Keychain或1Password),以便稍后在本教程中使用它们。

terraform 如何创建数据盘(DevOps系列Terraform)(3)

保存凭据后,单击“关闭”(两次),您将进入用户列表。单击刚刚创建的用户,然后选择“权限”选项卡。默认情况下,新的IAM用户无权在AWS账户中执行任何操作。为了能够将Terraform用于此博客文章系列中的示例,请添加以下权限:

  • AmazonEC2FullAccess: 本博客文章需要。
  • AmazonS3FullAccess: 用于如何管理Terraform状态。
  • AmazonDynamoDBFullAccess: 用于如何管理Terraform状态。
  • AmazonRDSFullAccess: 用于如何用Terraform模块来创建复用的基础设施。
  • CloudWatchFullAccess: 用于Terraform小技巧:循环、if 语句等。
  • IAMFullAccess: 用于Terraform小技巧:循环、if 语句等。

terraform 如何创建数据盘(DevOps系列Terraform)(4)

安装Terraform

Terraform 是用Golang 开发的,为所有支持的平台分发二进制包。方便安装和使用,具体可以查看如下官方文档:https://www.terraform.io/。安装完成后,运行Terraform 会看到如下信息:

$ terraform Usage: terraform [-version] [-help] <command> [args] (...)

为了使Terraform能够在您的AWS中执行相应更改,您需要为之前创建的用户配置AWS凭据。有几种方法可以执行此操作,其中最简单的方法之一是设置以下环境变量:

export AWS_ACCESS_KEY_ID=(your access key id) export AWS_SECRET_ACCESS_KEY=(your secret access key)

部署单台服务器

Terraform代码是用HCL语言编写然后以.tf扩展名保存。它是一种声明性语言,因此您的目标是描述您想要的基础架构,Terraform会知道如何创建它。 Terraform可以在各种平台上创建基础设施,或称为提供商,包括AWS,Azure,Google Cloud,DigitalOcean和许多其他平台。

您可以在任何文本编辑器中编写Terraform代码。如果你四处搜索,你可以找到大多数编辑器都支持Terraform语法高亮(注意,你可能需要搜索单词“HCL”而不是“Terraform”),包括vim,emacs,Sublime Text,Atom,Visual Studio Code,和IntelliJ(后者甚至支持重构,查找用法)。

使用Terraform的第一步通常是配置你所想要使用的服务商。创建一个名为main.tf的文件,并在其中放入以下代码:

provider "aws" { region = "us-east-2" }

这就是告诉Terraform您将使用AWS提供商并希望在us-east-2区域部署您的基础架构(AWS在全球范围内拥有数据中心,分为区域和可用区域以及我们东部 -2是美国俄亥俄州数据中心的名称)。 您可以为AWS配置其他设置,但是对于此示例,由于您已将凭据配置为环境变量,因此您只需指定该区域。

对于每个服务商,您可以创建许多不同类型的资源,例如服务器,数据库和负载均衡器。 在我们部署整个服务器集群之前,让我们首先弄清楚如何部署单个服务器来响应“Hello,World”的HTTP请求。 在AWS lingo中,服务器称为EC2实例。 将以下代码添加到main.tf,后者使用aws_instance资源部署EC2实例:

resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" }

Terraform资源的一般语法是:

resource "<PROVIDER>_<TYPE>" "<NAME>" { [CONFIG …] }

其中PROVIDER是服务商的名称(例如,aws),TYPE是在该提供者中创建的资源的类型(例如,instance),NAME是可以在整个Terraform代码中用于引用该资源的标识符(例如, example),CONFIG由一个或多个特定于该资源的参数组成(例如,ami =“ami-0c55b159cbfafe1f0”)。 对于aws_instance资源,有许多不同的参数,但是现在,您只需要设置以下参数:

  • ami:要在EC2实例上运行Amazon Machine Image(AMI)。 您可以在AWS Marketplace中找到免费和付费的AMI,也可以使用Packer等工具创建自己的AMI。 上面的代码将ami参数设置为在us-east-2中的Ubuntu 18.04 AMI的ID。 此AMI可免费使用。
  • instance_type:运行EC2实例的类型。 每种类型的EC2实例都提供不同数量的CPU,内存,磁盘空间和网络容量。 官方的EC2 Instance Types页面列出了所有可用的选项和每个成本。 上面的示例使用t2.micro,它具有一个虚拟CPU,1GB内存,并且是AWS免费层的一部分。

在终端中,进入创建main.tf的文件夹,然后运行terraform init命令:

$ terraform init Initializing the backend... Initializing provider plugins... - Checking for available provider plugins... - Downloading plugin for provider "aws" (...) * provider.aws: version = "~> 2.10" Terraform has been successfully initialized!

terraform二进制文件包含Terraform的基本功能,但它不附带任何服务商的代码(例如,AWS,Azure,GCP等),因此在首次开始使用Terraform时,您需要 运行terraform init告诉Terraform扫描代码,找出你正在使用的云服务商,并为它们下载代码。 默认情况下,服务商代码将下载到.terraform文件夹中,该文件夹是Terraform的临时目录(您可能希望将其添加到.gitignore)。 稍后您将看到init命令和.terraform文件夹的一些其他用途。 现在,请注意,每次开始使用新的Terraform代码时都需要运行init,并且多次运行init是安全的(命令是幂等的)。

现在您已下载服务商的代码了,请运行terraform plan命令:

$ terraform plan Refreshing Terraform state in-memory prior to plan... (...) aws_instance.example ami: "ami-2d39803a" availability_zone: "<computed>" ebs_block_device.#: "<computed>" ephemeral_block_device.#: "<computed>" instance_state: "<computed>" instance_type: "t2.micro" key_name: "<computed>" network_interface_id: "<computed>" placement_group: "<computed>" private_dns: "<computed>" private_ip: "<computed>" public_dns: "<computed>" public_ip: "<computed>" root_block_device.#: "<computed>" security_groups.#: "<computed>" source_dest_check: "true" subnet_id: "<computed>" tenancy: "<computed>" vpc_security_group_ids.#: "<computed>" Plan: 1 to add, 0 to change, 0 to destroy.

plan命令可让您在实际执行之前查看Terraform将执行的操作。 这是一种很好的方法,可以在实际操作变更之前检查您的更改操作是否符合预期。 plan命令的输出有点像diff命令的输出:带有加号( )的资源将会创建,带有减号( - )的资源将被删除,带有波浪号(〜)的资源将符号将被就地修改。 在上面的输出中,您可以看到Terraform正在计划只创建单个EC2实例而,这正是我们想要的。

要实际创建实例,请运行terraform apply命令:

$ terraform apply (...) Terraform will perform the following actions: # aws_instance.example will be created resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" arn = (known after apply) associate_public_ip_address = (known after apply) availability_zone = (known after apply) cpu_core_count = (known after apply) cpu_threads_per_core = (known after apply) get_password_data = false host_id = (known after apply) id = (known after apply) instance_state = (known after apply) instance_type = "t2.micro" ipv6_address_count = (known after apply) ipv6_addresses = (known after apply) key_name = (known after apply) (...) } Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:

您会注意到apply命令显示输出和之前plan命令输出一样的,并要求您确认是否确实要继续执行此计划。 因此,虽然plan可作为单独的命令使用,但它主要用于快速健全性检查和代码审查,并且大多数情况下您将直接运行并查看它的计划输出。

输入“yes”并按Enter键以部署EC2实例:

Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_instance.example: Creating… aws_instance.example: Still creating… [10s elapsed] aws_instance.example: Still creating… [20s elapsed] aws_instance.example: Still creating… [30s elapsed] aws_instance.example: Creation complete after 38s Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

恭喜,您刚刚用Terraform部署了一台服务器! 要验证这一点,您可以登录EC2控制台,您将看到如下内容:

terraform 如何创建数据盘(DevOps系列Terraform)(5)

生效了,但这不是最激动人心的例子。 首先,Instance没有名称。 要添加一个,您可以向EC2实例添加标记:

resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" tags = { Name = "terraform-example" } }

再次运行terraform apply命令来看看会有什么操作结果:

$ terraform apply aws_instance.example: Refreshing state... (...) Terraform will perform the following actions: # aws_instance.example will be updated in-place ~ resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" availability_zone = "us-east-2b" instance_state = "running" (...) tags = { "Name" = "terraform-example" } (...) } Plan: 0 to add, 1 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:

Terraform会跟踪它为这组配置文件创建的所有资源,因此它知道您的EC2实例已经存在(注意Terraform在运行apply命令时说“刷新状态...”),它可以显示当前部署的内容以及Terraform代码中的内容两者之间的差异 (这是使用声明性语言而不是程序性语言的优势之一)。 前面的差异显示Terraform想要创建一个名为“Name”的单个标签,这正是您所需要的,因此输入“yes”并按Enter键。

当再次刷新EC2控制台时,你会看到:

terraform 如何创建数据盘(DevOps系列Terraform)(6)

部署单台Web服务器

下一步是在此实例上运行Web服务器。 在一个真实的用例中,您可能会安装一个功能齐全的Web框架,如Ruby on Rails或Django,但为了保持这个简单的示例,我们将运行一个简单的Web服务器,使用其他哪里(https://gist.github.com/willurd/5720255)中的代码,它始终返回文本 “Hello,World”:

#!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p 8080 &

这是一个bash脚本,它将文本“Hello,World”写入index.html,并使用busybox(在Ubuntu上默认安装)在端口8080上运行Web服务器,以便在URL“/”处提供该文件。 我们用nohup运行busybox命令并在命令末尾放置一个&,以确保即使在此脚本退出后Web服务器仍然运行,以便Web服务器在后台进程中运行,并且脚本可以退出而不是被Web服务器永远阻止。

如何让EC2实例运行此脚本? 通常,您可以使用像Packer这样的工具来创建安装了Web服务器的自定义AMI,而不是使用什么也没有的Ubuntu AMI。 但同样,为了保持这个例子的简单,我们将上面的脚本作为EC2 Instance的用户数据的一部分运行,AWS将在实例启动时执行:

resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" user_data = <<-EOF #!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p 8080 & EOF tags = { Name = "terraform-example" } }

<<-EOF 和 EOF 是 Terraform的界定符语法,它允许您无需使用\n而可以创建多行字符串。

在此Web服务器工作之前,您需要再做一件事。 默认情况下,AWS不允许来自EC2实例的任何传入或传出流量。 要允许EC2实例接收端口8080上的流量,您需要创建一个安全组:

resource "aws_security_group" "instance" { name = "terraform-example-instance" ingress { from_port = 8080 to_port = 8080 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } }

此代码创建一个名为aws_security_group的新资源(请注意AWS服务商的所有资源都以aws_开头),并指定此组允许来自CIDR为0.0.0.0/0的端口为8080的TCP流量请求。CIDR是指定IP地址范围的简明方法。 例如,10.0.0.0/24的CIDR表示10.0.0.0和10.0.0.255之间的所有IP地址。 CIDR0.0.0.0/0是包含所有可能IP地址的IP地址范围,因此该安全组允许来自任何IP针对端口为8080上的流量请求。 对于在IP地址范围和CIDR表示法之间进行转换的便捷计算器,可以看:http://tool.chinaz.com/Tools/subnetmask。

仅仅创建一个安全组是不够的; 您还需要通过将安全组的ID传递给aws_instance资源的vpc_security_group_ids参数来告知EC2实例来使用它。 要做到这一点,您首先需要了解Terraform表达式。

Terraform中的表达式是任何返回值的东西。 你已经看过最简单的表达式,文字,如字符串(例如“ami-0c55b159cbfafe1f0”)和数字(例如5)。 Terraform支持很多其他类型的表达式您将在本专题系列中看到的。

一种特别有用的表达式是引用,它允许您从代码的其他部分访问值。 要访问安全组资源的ID,您将需要使用资源属性引用( resource attribute reference),它使用以下语法:

<PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>

其中PROVIDER是云服务商的名称(例如,aws),TYPE是资源的类型(例如,security_group),NAME是该资源的名称(例如,安全组被命名为“instance”),并且ATTRIBUTE要么是 该资源的一个参数(例如,name)或资源导出的一个属性(您可以在文档中找到每个资源的可用属性列表 - 例如,这里(https://www.terraform.io/docs/providers/aws/r/security_group.html#attributes-reference)是aws_security_group的属性)。 安全组导出一个名为id的属性,因此引用它的表达式如下所示:

aws_security_group.instance.id

您可以在aws_instance的vpc_security_group_ids参数中使用此安全组ID:

resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" vpc_security_group_ids = [aws_security_group.instance.id] user_data = <<-EOF #!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p 8080 & EOF tags = { Name = "terraform-example" } }

将引用从一个资源添加到另一个资源时,将创建隐式依赖项。 Terraform解析这些依赖关系,从它们构建依赖关系图,并使用它来自动确定应该以什么顺序创建资源。 例如,如果您要从头开始部署此代码,Terraform会知道它需要在EC2实例之前创建安全组,因为EC2实例引用了安全组的ID。

当Terraform遍历您的依赖关系树时,它将尽可能多地并行创建资源,这意味着它可以相当有效地应用您的更改。 这就是声明性语言的美妙之处:你只需指定你想要的东西,Terraform就会找出实现它的最有效方法。

如果运行apply命令,您将看到Terraform想要添加安全组并将EC2实例替换为具有新用户数据的新实例:

$ terraform apply (...) Terraform will perform the following actions: # aws_instance.example must be replaced -/ resource "aws_instance" "example" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" (...) user_data = "c765373..." # forces replacement ~ vpc_security_group_ids = [ - "sg-871fa9ec", ] -> (known after apply) (...) } # aws_security_group.instance will be created resource "aws_security_group" "instance" { arn = (known after apply) description = "Managed by Terraform" egress = (known after apply) id = (known after apply) ingress = [ { cidr_blocks = [ "0.0.0.0/0", ] description = "" from_port = 8080 ipv6_cidr_blocks = [] prefix_list_ids = [] protocol = "tcp" security_groups = [] self = false to_port = 8080 }, ] name = "terraform-example-instance" (...) } Plan: 2 to add, 0 to change, 1 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:

plan输出中的 - / 表示“替换”; 寻找文本“强制更换”以找出迫使Terraform进行替换的内容。 使用EC2实例,对许多属性的更改将强制终止原始实例并创建一个全新的实例(这是不可变基础设施范例的示例)。 值得一提的是,在更换Web服务器时,该Web服务器的任何用户都会遇到停机; 您将看到如何在Terraform提示和技巧中使用Terraform进行零停机部署:循环,if语句和陷阱。

由于计划看起来不错,请输入“是”,您将看到新的EC2实例部署:

terraform 如何创建数据盘(DevOps系列Terraform)(7)

在屏幕底部的描述面板中,您还将看到此EC2实例的公共IP地址。 给它一两分钟启动,然后使用Web浏览器或curl等工具向此IP地址的端口8080发出HTTP请求:

$ curl http://<EC2_INSTANCE_PUBLIC_IP>:8080 Hello, World

好了,您现在有一个在AWS中运行的Web服务器了!

部署一台可配置的Web 服务器

您可能已经注意到Web服务器代码在安全组和用户数据配置中都重复配置了端口8080。 这违反了“不要重复”(DRY)原则:每一条规则都必须在系统中具有单一,明确,权威的表示。 如果您在两个地方复制/粘贴端口号,则在一个地方更新它很容易,但很容易忘记在另一个地方进行相同的更改。

为了使您的代码符合原则以及更具有可配置性,Terraform允许您定义输入变量(input variables)。 声明变量的语法是:

variable "NAME" { [CONFIG ...] }

变量声明的主体可以包含三个参数,所有参数都是可选的:

  • description:使用此参数记录变量的使用方式是个好方式。 您的同事不仅能够在阅读代码时看到此描述,还能在运行planapply命令时看到此描述(您很快就会看到这样的示例)。
  • default:有许多方法可以为变量赋值,包括在命令行(使用-var选项),通过文件(使用-var-file选项)或通过环境变量(Terraform 会查找名称为TF_VAR__ <variable_name>的环境变量)。 如果未传入任何值,则变量将回退到此默认值。 如果没有默认值,Terraform将以交互方式提示用户输入一个。
  • type: 这允许您对用户传入的变量强制执行类型约束.Terraform支持许多类型约束,包括字符串,数字,布尔,列表,映射,集合,对象,元组和any。 如果未指定类型,则Terraform假定类型为any。

对于此次Web服务器示例,以下是如何创建存储端口号的变量:

variable "server_port" { description = "The port the server will use for HTTP requests" type = number }

请注意,server_port输入变量没有默认值,因此如果您立即运行apply命令,Terraform将以交互方式提示您输入server_port的值并显示变量的description:

$ terraform apply var.server_port The port the server will use for HTTP requests Enter a value:

如果您不想用交互式提示,可以通过-var命令行选项为变量提供值:

$ terraform apply -var "server_port=8080"

您还可以通过名为TF_VAR_ <name>的环境变量设置变量,其中<name>是您要设置的变量的名称:

$ export TF_VAR_server_port=8080 $ terraform apply

如果您不想在每次运行planapply时都需要记住额外的命令行参数,则可以指定默认值:

variable "server_port" { description = "The port the server will use for HTTP requests" type = number default = 8080 }

要使用Terraform代码中输入变量的值,可以使用称为变量引用(variable reference)的新类型的表达式,该表达式具有以下语法:

var.<VARIABLE_NAME>

例如,以下是如何将安全组的from_portto_port参数设置为server_port变量的值:

resource "aws_security_group" "instance" { name = "terraform-example-instance" ingress { from_port = var.server_port to_port = var.server_port protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } }

在User Data脚本中设置端口时,使用相同的变量也是一个好主意。 要在字符串文字中使用引用,您需要使用一种称为插值(interpolation)的新类型的表达式,它具有以下语法:

"${...}"

您可以在花括号中放置任何有效的引用,Terraform会将其转换为字符串。 例如,以下是如何在User Data字符串中使用var.server_port

user_data = <<-EOF #!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p "${var.server_port}" & EOF

除输入变量外,Terraform还允许您使用以下语法定义输出变量:

output "<NAME>" { value = <VALUE> [CONFIG ...] }

NAME是输出变量的名称,VALUE可以是您要输出的任何Terraform表达式。 CONFIG可以包含两个附加参数,两个都是可选的:

  • description:使用此参数来记录输出变量中包含的数据类型总是一个好主意。
  • sensitive:将此参数设置为true可告知Terraform不要在terraform apply结束时记录此输出。 如果输出变量包含敏感材料或机密(例如密码或私钥),这将非常有用。

例如,您可以提供IP地址作为输出变量,而不必手动浏览EC2控制台以查找服务器的IP地址:

output "public_ip" { value = aws_instance.example.public_ip description = "The public IP of the web server" }

此代码再次使用属性引用,这次引用aws_instance资源的public_ip属性。 如果再次运行apply命令,Terraform将不会应用任何更改(因为您没有改变任何资源),但它会在最后显示新的输出:

$ terraform apply (...) aws_security_group.instance: Refreshing state... aws_instance.example: Refreshing state... Apply complete! Resources: 0 added, 0 changed, 0 destroyed. Outputs: public_ip = 54.174.13.5

如您所见,运行terraform apply后,输出变量会显示在控制台中,Terraform代码的哪些用户可能会觉得有用(例如,您现在知道在部署Web服务器后要测试的IP)。 您可以还可以使用terraform output命令列出所有输出而不应用任何更改:

$ terraform output public_ip = 54.174.13.5

您可以运行terraform output <OUTPUT_NAME>以查看名为<OUTPUT_NAME>的特定输出的值:

$ terraform output public_ip 54.174.13.5

这对于脚本编写尤其方便。 例如,您可以创建运行terraform apply的部署脚本来部署Web服务器,使用terraform output public_ip来获取其公共IP,并在IP上运行curl快速简单测试,以验证部署是否有效。

输入和输出变量也是创建可配置和可复用的基础设施代码的基本要素,您将会在稍后的系列文章如何使用Terraform模块创建可重用基础架构中看到更多详细使用。

部署一组Web 服务器集群

运行单个服务器是一个良好的开端,但在生产环境中,单个服务器存在单点故障风险。 如果该服务器崩溃,或者由于流量太大而导致服务器不堪重负,则用户将无法再访问您的站点。 解决方案是运行服务器群集,绕过服务器路由,并根据流量调整群集的大小。

手动管理这样的集群会有很多工作和负担。 幸运的是,您可以让AWS使用Auto Scaling Group(ASG)来处理它。 ASG可以自动启动EC2实例集群,监控其运行状况,自动重启故障节点,并根据需求调整集群大小。

创建ASG的第一步是创建启动配置,该配置指定如何配置ASG中的每个EC2实例。 从早期部署单个EC2实例开始,您已经确切地知道如何配置它,并且您可以在aws_launch_configuration资源中重用几乎完全相同的参数:

resource "aws_launch_configuration" "example" { image_id = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" security_groups = [aws_security_group.instance.id] user_data = <<-EOF #!/bin/bash echo "Hello, World" > index.html nohup busybox httpd -f -p "${var.server_port}" & EOF lifecycle { create_before_destroy = true } }

这里唯一新的东西是生命周期(lifecycle)设置。 Terraform支持多种生命周期设置,可让您自定义资源的创建和销毁方式。 create_before_destroy设置控制重新创建资源的顺序。 默认顺序是删除旧资源,然后创建新资源。 将create_before_destroy设置为true会反转此顺序,首先创建替换,然后删除旧替换。 由于对启动配置的每次更改都会创建全新的启动配置,因此您需要此设置以确保首先创建新配置,因此任何使用此启动配置的ASG可以更新以指向新的配置,然后删除旧配置。

现在,您可以使用aws_autoscaling_group资源创建ASG:

resource "aws_autoscaling_group" "example" { launch_configuration = aws_launch_configuration.example.id min_size = 2 max_size = 10 tag { key = "Name" value = "terraform-asg-example" propagate_at_launch = true } }

该ASG将在2到10个EC2实例之间运行(初始启动时默认为2),每个实例都标记为“terraform-asg-example”。 ASG使用引用来填充启动配置名称。

要使此ASG正常工作,您需要再指定一个参数:availability_zones。 此参数指定应部署EC2实例的可用区(AZ)。 每个AZ代表一个独立的AWS数据中心,因此通过跨多个AZ部署实例,即使某些AZ发生故障,也可确保您的服务可以继续运行。 您可以对AZ列表进行硬编码(例如将其设置为[“us-east-2a”,“us-east-2b”]),但这将无法维护或移植(例如,每个AWS账户都有权访问不同的AZ集合),所以更好的选择是使用数据源来获取您的AWS账户中的子网列表。

数据源表示每次运行Terraform时从服务商(在本例中为AWS)中获取的只读信息。 将数据源添加到Terraform配置不会创建任何新内容; 它只是一种查询服务商的API数据的方法,并使这些数据可用于Terraform的其余代码。 每个Terraform服务商都公开了各种数据源。 例如,AWS提供商包括用于查找VPC数据,子网数据,AMI ID,IP地址范围,当前用户身份等的数据源。

使用数据源的语法与资源的语法非常相似:

data "<PROVIDER>_<TYPE>" "<NAME>" { [CONFIG ...] }

PROVIDER是服务商的名称(例如,aws),TYPE是您要使用的数据源类型(例如,vpc),NAME是您可以在整个Terraform代码中用来引用此数据源的标识符,同时CONFIG 由一个或多个特定于该数据源的参数组成。 例如,以下是如何使用aws_availability_zones数据源获取AWS账户中的AZ列表:

data "aws_availability_zones" "all" {}

要从数据源中获取数据,请使用以下属性引用语法:

data.<PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>

例如,要从aws_availability_zones数据源获取AZ名称列表,请使用以下命令:

data.aws_availability_zones.all.names

使用此值设置aws_autoscaling_group资源的availability_zone参数:

rresource "aws_autoscaling_group" "example" { launch_configuration = aws_launch_configuration.example.id availability_zones = data.aws_availability_zones.all.names min_size = 2 max_size = 10 tag { key = "Name" value = "terraform-asg-example" propagate_at_launch = true } }

部署一台负载均衡器

此时,您可以部署ASG,但是您会遇到一个小问题:您现在拥有多个服务器,每个服务器都有自己的IP地址,但您通常只希望为最终用户提供一个IP。 解决此问题的一种方法是部署负载均衡器以在您的服务器之间分配流量,并为所有用户提供负载均衡器的IP(实际上是DNS名称)。 创建高可用性和可伸缩性的负载均衡器需要大量工作。 同样,您可以让AWS为您处理,这次使用亚马逊的Elastic Load Balancer(ELB)服务。

AWS提供三种不同类型的负载均衡器:

  1. 应用程序负载均衡器(ALB):最适合HTTP和HTTPS流量。
  2. 网络负载均衡器(NLB):最适合TCP和UDP流量。
  3. 经典负载均衡器(CLB):这是“传统”负载均衡器,早于ALB和NLB。 它可以执行HTTP,HTTPS和TCP,但提供的功能远远少于ALB或NLB。

由于我们的Web服务器使用HTTP,ALB将是最合适的,但它需要更多的代码和更多解释,因此为了使这篇长篇博文不会变得更长,我们将使用更简单的CLB。

您可以使用aws_elb资源创建CLB:

resource "aws_elb" "example" { name = "terraform-asg-example" availability_zones = data.aws_availability_zones.all.names }

这将创建一个ELB,该ELB将部署在您帐户中的所有AZ中。 AWS负载均衡器不包含单个服务器,而是可以在单独的AZ(即独立的数据中心)中运行的多个服务器。 AWS将根据流量自动调整负载均衡器服务器的数量,并在其中一台服务器发生故障时处理故障转移,因此您可以获得可扩展性和高可用性。

请注意,在您告诉CLB如何路由请求之前,上面的aws_elb代码没有做太多工作。 为此,您需要添加一个或多个侦听器,这些侦听器指定CLB应侦听的端口以及应将请求路由到的端口:

resource "aws_elb" "example" { name = "terraform-asg-example" availability_zones = data.aws_availability_zones.all.names # This adds a listener for incoming HTTP requests. listener { lb_port = 80 lb_protocol = "http" instance_port = var.server_port instance_protocol = "http" } }

在上面的代码中,我们告诉CLB在端口80(HTTP的默认端口)上接收HTTP请求,并将它们路由到ASG中Instances使用的端口。 请注意,默认情况下,CLB不允许任何传入或传出流量(就像EC2实例一样),因此您需要添加新的安全组以明确允许端口80上的入站请求和所有出站请求(正如您将很快看到的那样,后者允许CLB执行健康检查):

resource "aws_security_group" "elb" { name = "terraform-example-elb" # Allow all outbound egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } # Inbound HTTP from anywhere ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } }

您现在需要通过添加security_groups参数来告诉CLB使用此安全组:

resource "aws_elb" "example" { name = "terraform-asg-example" security_groups = [aws_security_group.elb.id] availability_zones = data.aws_availability_zones.all.names # This adds a listener for incoming HTTP requests. listener { lb_port = var.elb_port lb_protocol = "http" instance_port = var.server_port instance_protocol = "http" } }

CLB还有另外一个好用的技巧:它可以定期检查你的EC2实例的运行状况,如果一个实例不健康,它会自动停止路由流量到它。 让我们添加一个HTTP运行状况检查,其中CLB每隔30秒向每个EC2实例的“/”URL发送一个HTTP请求,并且如果它以200 OK响应,则将实例标记为正常:

resource "aws_elb" "example" { name = "terraform-asg-example" security_groups = [aws_security_group.elb.id] availability_zones = data.aws_availability_zones.all.names health_check { target = "HTTP:${var.server_port}/" interval = 30 timeout = 3 healthy_threshold = 2 unhealthy_threshold = 2 } # This adds a listener for incoming HTTP requests. listener { lb_port = var.elb_port lb_protocol = "http" instance_port = var.server_port instance_protocol = "http" } }

CLB如何知道向哪些EC2实例发送请求? 您可以使用CLB的实例参数将EC2实例的静态列表附加到ELB,但是对于ASG,实例将一直动态启动和终止,因此无法工作。 相反,您可以使用aws_autoscaling_group资源的load_balancers参数告诉ASG在CLB中注册每个实例:

resource "aws_autoscaling_group" "example" { launch_configuration = aws_launch_configuration.example.id availability_zones = data.aws_availability_zones.all.names min_size = 2 max_size = 10 load_balancers = [aws_elb.example.name] health_check_type = "ELB" tag { key = "Name" value = "terraform-asg-example" propagate_at_launch = true } }

请注意,我们还将ASG的health_check_type配置为“ELB”。 默认的health_check_type是“EC2”,这是一个最小化的运行状况检查,只考虑实例如果AWS虚拟机管理程序检测到服务器完全关闭或无法访问不健康时候,就确定为实例不健康状态。 “ELB”健康检查更加健壮,因为它告诉ASG使用CLB的健康检查来确定实例是否健康,如果CLB将其报告为不健康,则自动替换实例。 这样,实例不仅当它们down机时候会被替换,而且如果它们因为内存不足或关键进程崩溃而停止提供请求也会被替换。

在部署负载均衡器之前要做的最后一件事:让我们将其DNS添加为输出,以便更容易测试是否正常:

output "clb_dns_name" { value = aws_elb.example.dns_name description = "The domain name of the load balancer" }

运行terraform apply并读取计划输出。 您应该看到原始单个EC2实例正在被移除,Terraform将在原来的地方创建启动配置,ASG,ALB和安全组。 如果计划看起来正常,键入“yes”并按Enter键。 apply完成后,您应该看到clb_dns_name输出:

Outputs: clb_dns_name = terraform-asg-example-123.us-east-2.elb.amazonaws.com

复制此URL。 实例启动会花费几分钟并在CLB中显示为健康状态。 在此期间,您可以检查已部署的内容。 打开EC2控制台的ASG部分,您应该看到已创建ASG:

terraform 如何创建数据盘(DevOps系列Terraform)(8)

如果切换到Instances选项卡,您将看到两个实例正在启动:

terraform 如何创建数据盘(DevOps系列Terraform)(9)

最后,如果切换到Load Balancers选项卡,您将看到您的CLB:

terraform 如何创建数据盘(DevOps系列Terraform)(10)

等待“状态”指示显示“2 of 2 instacncess in service”。这通常需要1-2分钟。 看到之后,测试先前复制的clb_dns_name输出:

$ curl http://terraform-asg-example-123.us-east-2.elb.amazonaws.com Hello, World

成功了! CLB将流量路由到您的EC2实例。 每次点击URL时,它都会选择不同的实例来处理请求。 您现在拥有一个完全可用的Web服务器集群! 提醒一下,上面示例的示例代码位于:https://github.com/yangvipguang/intro-to-terraform。

此时,您可以看到群集如何响应启动新实例或关闭旧实例。 例如,转到Instances选项卡,通过选中其复选框,选择顶部的“Actions”按钮,然后将“Instance State”设置为“Terminate”来终止其中一个Instances。继续测试CLB URL和你 即使在终止实例时,也应该为每个请求获得“200 OK”,因为CLB将自动检测到Instance已关闭并停止路由到它。 更有趣的是,在Instance关闭后的短时间内,ASG将检测到正在运行的实例少于2个,并自动启动一个新实例来替换它(自我修复!)。 您还可以通过更改min_sizemax_size参数或向Terraform代码添加desired_size参数以及重新运行apply来查看ASG如何调整自身大小。

当然,ASG还有许多其他方面,我们在这里没有涉及。 对于实际部署,您需要将IAM角色附加到EC2实例,设置机制以在零停机时间内更新ASG中的EC2实例,并配置自动扩展策略以调整ASG的大小以响应负载。

清理删除

当您用完Terraform时,最好删除您创建的所有资源,以便AWS不会向您收取费用。 由于Terraform会跟踪您创建的资源,因此清理很简单。 您需要做的就是运行destroy命令:

$ terraform destroy (...) Terraform will perform the following actions: # aws_autoscaling_group.example will be destroyed - resource "aws_autoscaling_group" "example" { (...) } # aws_launch_configuration.example will be destroyed - resource "aws_launch_configuration" "example" { (...) } # aws_lb.example will be destroyed - resource "aws_lb" "example" { (...) } (...) Plan: 0 to add, 0 to change, 8 to destroy. Do you really want to destroy all resources? Terraform will destroy all your managed infrastructure, as shown above. There is no undo. Only 'yes' will be accepted to confirm. Enter a value:

输入“yes”并按Enter键后,Terraform将使用尽可能多的并行性构建依赖关系图并按正确的顺序删除所有资源。 大约一分钟后,清理完成。

总结

您现在已经掌握了如何使用Terraform的基本知识。 声明性语言可以很容易地准确描述您要创建的基础设施。 plan命令允许您在部署之前验证更改并捕获错误。 变量,引用和依赖项允许您保持代码非重复性配置和高效。

但是,我们还是只能算是入门了。 在本系列的第3部分“如何管理Terraform状态”中,我们将展示Terraform如何跟踪它已经创建的基础架构,以及对如何构建Terraform代码所产生的深远影响。 在本系列的第4部分中,我们将展示如何使用Terraform模块创建可重用的基础设施

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页