Infrasturcture as Code
对于管理服务器等资源,Ansible什么的可能就不大好用了,我们可以使用Terraform进行弹性创建等等,并且可以将我们的配置等等信息文本化,从而通过git等版本管理系统来进行维护。
安装
官方网站
要素
语法
1 2 3 4
| <BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" { # Block body <IDENTIFIER> = <EXPRESSION> # Argument }
|
1 2 3 4 5 6 7 8
| terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.27" } } }
|
terraform
块的作用是让Terraform知道去Terraform Registry下载什么provider。上述代码作用:hashicorp/aws
作为aws的提供源,hashicorp/aws
是registry.terraform.io/hashicorp/aws
的简写。我们同时可以指定version
,如果不指定,会自动下载最新的。
Providers
1 2 3 4
| provider "aws" { profile = "default" region = "us-west-2" }
|
provider
块用来设置provider。一个provider是一个插件,用来翻译Terraform用户和云服务之间的API。provider负责理解API和管理资源。profile
属性用来选择储存在.aws文件里的credentials。我们不要在.tf
文件里硬编码我们的密钥等信息,而且通过指定来引用。
Resources
1 2 3 4 5 6 7 8
| resource "aws_instance" "example" { ami = "ami-830c94e3" instance_type = "t2.micro"
tags = { Name = "ExampleInstance" } }
|
resource
块的用来定义资源,一个资源可以是物理的资源比如一台EC2实例,或者是一个逻辑上的资源,比如一个Docker容器。一个resource
块包含2个字符串,这里是"aws_instance" "example"
,表示资源的类型是aws_instance
,且被命名为example
。
剩下的字段都是这个资源的参数。比如:服务器配置,镜像名称,或者VPC的ID
变换配置
服务器配置一直在变化,Terraform同样可以帮助我们管理这些变化,Terraform是声明型配置,当我们修改Terraform配置后,Terraform只会执行需要修改的部分来达到我们想要的状态。所以我们可以通过版本管理追溯到我们的架构的历史变化。
1 2 3 4 5
| resource "aws_instance" "example" { - ami = "ami-830c94e3" + ami = "ami-08d70e59c07c61a3a" instance_type = "t2.micro" }
|
当上诉变化发生后,AWS provider知道当一个实例创建后就不能改变ami了,所以Terraform将会释放现有的实例,然后创建一个新的实例。
指令
1 2 3 4 5 6 7 8 9 10 11 12
| treeaform init
terraform fmt
terraform validate
terraform plan
terraform apply
terraform destory
|
可以把这个项目初始化为git库,这样可以通过版本管理,来管理你的配置
下面这个例子会启动一个ESC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.27" } } }
provider "aws" { profile = "default" region = "us-west-2" }
resource "aws_instance" "example" { ami = "ami-830c94e3" instance_type = "t2.micro"
tags = { Name = "ExampleInstance" } }
|
高级操作
变量
同Ansible,我们需要结构化我们的项目,也就是我们需要使用模板等复用我们的设置,或者调整一些参数。以下代码定义了一个instance_name
变量,类型为string
。
1 2 3 4 5
| variable "instance_name" { description = "Value of the Name tag for the EC2 instance" type = string default = "ExampleInstance" }
|
然后我们可以这样进行使用
1 2 3 4 5 6 7 8 9
| resource "aws_instance" "example" { ami = "ami-08d70e59c07c61a3a" instance_type = "t2.micro"
tags = { - Name = "ExampleInstance" + Name = var.instance_name } }
|
Output
我们部署完后,可能会需要一些输出,比如服务器的IP,私钥啥的,就可以使用output
块
1 2 3 4 5 6 7 8 9
| output "instance_id" { description = "ID of the EC2 instance" value = aws_instance.example.id }
output "instance_public_ip" { description = "Public IP address of the EC2 instance" value = aws_instance.example.public_ip }
|
MAP
我们可以配置一个MAP类型的变量来对应不同情况
instance.tf
1 2 3 4
| resource "aws_instance" "example" { ami = "${lookup(var.AMIS, var.AWS_REGION)}" instance_type = "t2.micro" }
|
vars.tf
1 2 3 4 5 6 7 8 9 10 11 12 13
| variable "AWS_ACCESS_KEY" {} variable "AWS_SECRET_KEY" {} variable "AWS_REGION" { default = "us-east-1" } variable "AMIS" { type = "map" default = { us-east-1 = "ami-13be557e" us-west-2 = "ami-06b94666" eu-west-1 = "ami-0d729a60" } }
|
ami-id会会变化,记住去官网查看ec2/locator
安装软件
我们只讨论Ansible的情况,我们可以先运行特仍然form,输出IP地址,然后运行ansible-playbook
- 可以将他们通过脚本自动化
- 也有第三方库来自动化他们
上传文件
1 2 3 4
| provisioner "file" { source = "app.conf" destination = "/etc/myapp.conf" }
|
在Linux上使用ssh进行上传
1 2 3 4 5 6 7 8
| provisioner "file" { source = "script.sh" destination = "/opt/script.sh" connection { user = "${car.instance_username}" password = "${var.instance_password}" } }
|
ec2-user
是AWS的默认用户,ubuntu
是Ubuntu Linux
SSH keypairs
instance.tf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| resource "aws_key_pair" "edward-key" { key_name = "mykey" public_key = "ssh-rsa my-public-key" }
resource "aws_instance" "example" { ami = "${lookup(var.AMIS, var.AWS_REGION)}" instance_type = "t2.micro" key_name = "${aws_key_pair.mykey.key_name}" }
provisioner "file" { source = "script.sh" destination = "/opt/script.sh" connection { user = "${car.instance_username}" password = "${file(var.path_to_private_key)}" } }
|
执行脚本
远程执行
1 2 3 4 5 6
| provisioner "remote-exec" { inline = [ "chmod +x /opt/script.sh" "/opt/script.sh arguments" ] }
|
输出
1 2 3
| output "ip" { value = "${aws_instance.example.public_ip}" }
|
1 2 3 4 5 6 7
| resource "aws_instance" "example" { ami = "${lookup(var.AMIS, var.AWS_REGION)}" instance_type = "t2.micro" provisioner "local-exec" { command = "echo ${aws_instance.example.public_ip} >> private_ips.txt" } }
|
模板
同理Ansile模板,创建一个模板文件
1 2
| #!/bin/bash echo "database-ip = ${myip}" >> /etc/myapp.conf
|
渲染
1 2 3 4 5 6 7
| data "template_file" "my-temlpate" { temlpate = "${file("temlpates/init.tpl")}"
vars { myip = "${aws_instance.database1.private_ip}" } }
|
引用
1 2 3 4 5
| resource "aws_instance" "example" { # ...
user_data = "${data.temlpate_file.my-temlpate.rendered}" }
|
State
Terraform会保存远程的状态在terraform.tfstate
,这个文件包含一些ID和Terraform创建的资源的属性,以便可以对资源的追踪管理和销毁。我们必须安全保存terraform.tfstate
文件,同时会有一个之前的状态叫做terraform.tfstate.backup
。当我们输入apply
后,就会生成上面2个文件,我们可以将这个2个文件加入版本控制系统,但是如果多人合作的情况下,这样会很容易产生git冲突。Terraform推荐将Store保存在远程服务器上。
手动管理State
我们也可以输入指令进行更高级的管理,我们可以输入以下指令来查看我们资源的列表
远程State
在真实的生成环境下,我们需要保持state安全且加密,同时负责此项目的人还可以管理,最好的方法就是远程管理state。Terraform的remote backends允许Terraform使用共享储存来保存数据。我们需要配置remote backend来启用远程state。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| terraform { + backend "remote" { + organization = "<ORG_NAME>" + workspaces { + name = "Example-Workspace" + } + } required_providers { aws = { source = "hashicorp/aws" version = "~> 3.27" } } }
|
Datasoureces
有些数据是变化的,我们可以通过datasources来进行获取,比如AMIs的列表,可用地区等等
1 2 3 4 5 6 7 8 9
| data "aws_ami" "example" { most_recent = true
owners = ["self"] tags = { Name = "app-server" Tested = "true" } }
|
模块
类似ansible的role,也就是可用重复利用,引入一个模块
1 2 3
| module "module-example" { source = "https://github.com/..." }
|
本地方式
1 2 3 4 5 6
| module "module-example" { source = "./..." region = "us-west-1" ip-range = "10.0.0.0/8" cluster-size = "3" }
|
对一个module文件夹,我们可用有下面的结构
moddule/vars.tf
1 2 3
| variable "region" {} variable "ip-range" {} variable "cluster-size" {}
|
moddule/cluster.tf
1 2 3
| resource "aws_instance" "instance_1" {...} resource "aws_instance" "instance_2" {...} resource "aws_instance" "instance_3" {...}
|
moddule/output.tf
1 2 3
| output "aws-cluster" { value = "${aws_instance.instance-1.public},${aws_instance.instance-2.public},${aws_instance.instance-3.public}" }
|
使用下面的指令来获取模块
VPC
也就是个人的虚拟网络。我们需要把我们的服务器或数据库放在里面,分默认的,和我们自定义的
VPC之间的机器不能通过私有IP进行交流,虽然可用公共IP,但不推荐,可用连接2个VPC,叫做peering
创建VPC
vpc.tf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| # Internet VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" instance_tenancy = "default" enable_dns_support = "true" enable_dns_hostnames = "true" enable_classiclink = "false" tags = { Name = "main" } }
# Subnets resource "aws_subnet" "main-public-1" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" map_public_ip_on_launch = "true" availability_zone = "eu-west-1a"
tags = { Name = "main-public-1" } }
resource "aws_subnet" "main-public-2" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" map_public_ip_on_launch = "true" availability_zone = "eu-west-1b"
tags = { Name = "main-public-2" } }
resource "aws_subnet" "main-public-3" { vpc_id = aws_vpc.main.id cidr_block = "10.0.3.0/24" map_public_ip_on_launch = "true" availability_zone = "eu-west-1c"
tags = { Name = "main-public-3" } }
resource "aws_subnet" "main-private-1" { vpc_id = aws_vpc.main.id cidr_block = "10.0.4.0/24" map_public_ip_on_launch = "false" availability_zone = "eu-west-1a"
tags = { Name = "main-private-1" } }
resource "aws_subnet" "main-private-2" { vpc_id = aws_vpc.main.id cidr_block = "10.0.5.0/24" map_public_ip_on_launch = "false" availability_zone = "eu-west-1b"
tags = { Name = "main-private-2" } }
resource "aws_subnet" "main-private-3" { vpc_id = aws_vpc.main.id cidr_block = "10.0.6.0/24" map_public_ip_on_launch = "false" availability_zone = "eu-west-1c"
tags = { Name = "main-private-3" } }
# Internet GW resource "aws_internet_gateway" "main-gw" { vpc_id = aws_vpc.main.id
tags = { Name = "main" } }
# route tables resource "aws_route_table" "main-public" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.main-gw.id }
tags = { Name = "main-public-1" } }
# route associations public resource "aws_route_table_association" "main-public-1-a" { subnet_id = aws_subnet.main-public-1.id route_table_id = aws_route_table.main-public.id }
resource "aws_route_table_association" "main-public-2-a" { subnet_id = aws_subnet.main-public-2.id route_table_id = aws_route_table.main-public.id }
resource "aws_route_table_association" "main-public-3-a" { subnet_id = aws_subnet.main-public-3.id route_table_id = aws_route_table.main-public.id }
|
默认情况下上面只有内网,所以我们需要一个gateway来访问外网,如果不需要外网访问则可不设置
nat.tf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| # nat gw resource "aws_eip" "nat" { vpc = true }
resource "aws_nat_gateway" "nat-gw" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.main-public-1.id depends_on = [aws_internet_gateway.main-gw] }
# VPC setup for NAT resource "aws_route_table" "main-private" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.nat-gw.id }
tags = { Name = "main-private-1" } }
# route associations private resource "aws_route_table_association" "main-private-1-a" { subnet_id = aws_subnet.main-private-1.id route_table_id = aws_route_table.main-private.id }
resource "aws_route_table_association" "main-private-2-a" { subnet_id = aws_subnet.main-private-2.id route_table_id = aws_route_table.main-private.id }
resource "aws_route_table_association" "main-private-3-a" { subnet_id = aws_subnet.main-private-3.id route_table_id = aws_route_table.main-private.id }
|
安全组
安全组绑定一个VPC来定义进出口的端口和协议等等
securitygroup.tf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| resource "aws_security_group" "allow-ssh" { vpc_id = aws_vpc.main.id name = "allow-ssh" description = "security group that allows ssh and all egress traffic" egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }
ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "allow-ssh" } }
|
EBS Volumes
创建额外的数据卷来保存数据,默认分配的磁盘会在服务器释放后释放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| resource "aws_instance" "example" { ami = var.AMIS[var.AWS_REGION] instance_type = "t2.micro"
# the VPC subnet subnet_id = aws_subnet.main-public-1.id
# the security group vpc_security_group_ids = [aws_security_group.allow-ssh.id]
# the public SSH key key_name = aws_key_pair.mykeypair.key_name
# user data user_data = data.template_cloudinit_config.cloudinit-example.rendered }
resource "aws_ebs_volume" "ebs-volume-1" { availability_zone = "eu-west-1a" size = 20 type = "gp2" tags = { Name = "extra volume data" } }
resource "aws_volume_attachment" "ebs-volume-1-attachment" { device_name = var.INSTANCE_DEVICE_NAME volume_id = aws_ebs_volume.ebs-volume-1.id instance_id = aws_instance.example.id skip_destroy = true # skip destroy to avoid issues with terraform destroy }
|
User Data
User Data可以用来做一些初始化的事情,比如安装一些包,初始化数据卷等等
aws_instance
参考