Terraform 最佳实践

0. 前言

Terraform 的基础用法请参考另一篇文章

对于复杂的架构,最好的办法就是直接调用已有的官方module,不必自己重复造轮子。当官方的module 不满足需求,或者需要改造的时候,就需要我们自己来写定制化的module了。

换而言之,使用单一模块来实现反复调用,多个模块组合调用(模块1的输出是模块2的输入)。

1. AWS 例子

在AWS 环境,创建一个VM。

1.1 目录结构:

.
├── dev
│   ├── main.tf
│   └── provider.tf
└── modules
    ├── ec2
    │   ├── instance.tf
    │   └── variables.tf
    └── vpc
        ├── networking.tf
        ├── output.tf
        └── variables.tf

modules目录下面两个子目录代表了两个模块,分别用于创建网络和虚拟机,variables.tf 和 output.tf 代表了输入和输出,用于模块的调用和模块的交互。dev目录下tf 文件用于调用module。

1.2 模块文件介绍

1.2.1 vpc部分

下面所有的代码都可以从官网的例子中拷贝,比如搜索“terraform vpc”,然后找示例。

创建VPC 以及子网

# file modules/vpc/networking.tf

resource "aws_vpc" "main" {
  cidr_block       = "${var.vpc_cidr}"

  tags = {
    Name = "RoyVPC"
  }
}

resource "aws_subnet" "main" {
  vpc_id     = "${aws_vpc.main.id}"
  cidr_block = "${var.subnet_cidr}"

  tags = {
    Name = "bastion"
  }
}

当前模块要调用的变量。

# file modules/vpc/variables.tf

variable "vpc_cidr" {
  default = "10.0.0.0/16"
}

variable "subnet_cidr" {
  default = "10.0.1.0/24"
}

其它模块要用到,需要output 输出。

# file modules/vpc/output.tf

output "subnet_id" {
  value = "${aws_subnet.main.id}"
}

1.2.2 ec2部分

这部分创建虚拟机。

# file modules/ec2/instance.tf

resource "aws_instance" "web" {
  ami           = "${var.ami_id}"
  instance_type = "${var.instance_type}"
  subnet_id     = "${var.subnet_id}"
  key_name      = "royzeng"

  tags = {
    Name = "RoyBastion"
  }
}

本模块中用到的变量

# file ec2/variables.tf

variable "instance_type" {
  default = "t2.micro"
}
variable "ami_id" {}
variable "subnet_id" {}

没有output,这是功能测试,不需要向下一个模块传递变量。

1.3 模块调用

# file dev/provider.tf
provider "aws" {
  region     = "us-east-1"
}

上面的部分可以不写,但不写的话,运行时会提醒输入。

# file  dev/main.tf
module "my_vpc" {
  source      = "../modules/vpc"
  vpc_cidr    = "192.168.0.0/16"
  subnet_cidr = "192.168.1.0/24"
}

module "my_ec2" {
  source        = "../modules/ec2"
  ami_id        = "ami-0756fbca465a59a30"
  instance_type = "t2.micro"
  subnet_id     = "${module.my_vpc.subnet_id}"
}

这个文件就是调用了两个模块,source 定义了从哪里调用模块,把变量输入进去。需要注意的是第二个模块,变量里面需要输入前一个模块的输出结果,它的调用格式是

  subnet_id     = "${module.my_vpc.subnet_id}"

表示变量 subnet_id 从模块的实例 my_vpc 中调用 subnet_id,而这个值要从output 去找。这表明了两个模块的联系。(模块 my_vpc 的输出是模块 my_ec2 的输入)

1.4 验证

$ cd dev
$ terraform init
....
$ terraform apply --auto-approve

1.5 实际的例子

上面的例子缺了很多组件,在实践中是没法用的。下面以带有公有子网和私有子网 (NAT) 的 VPC 来举例。

vpc-pub-pri

从这张图可以看出,有太多的组件要部署和配置,网络方面就包括 VPC,subnets, route table, internet gateway, NAT gateway。可以用已有模块来完成,不用自己写代码。(搜索 Terraform VPC module,找到官方模块)。

下面的例子可以用在生产环境中:调用官方vpc模块,部署一系列的组件;调用自定义的模块来创建 instance(vm)。官方模块的输出可以看说明,也可以看output.tf 源码。

# file main.tf
provider "aws" {
  region     = "eu-west-3"
}

module "my_vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "~> 2.0"

  name = "roy-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["eu-west-3a",]
  private_subnets = ["10.0.101.0/24",]
  public_subnets  = ["10.0.1.0/24",]

  enable_dns_hostnames   = true
  enable_nat_gateway     = true
  enable_vpn_gateway     = false

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

module "security-group" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 3.0"
  name        = "royweb-sg"
  vpc_id      = "${module.my_vpc.vpc_id}"
  ingress_rules            = ["http-80-tcp", "https-443-tcp", "ssh-tcp"]
  ingress_cidr_blocks      = ["0.0.0.0/0"]
  egress_rules             = ["all-all"]
  egress_cidr_blocks       = ["0.0.0.0/0"]
}


module "my_front_ec2" {
  source                 = "terraform-aws-modules/ec2-instance/aws"
  version                = "~> 2.0"

  name                   = "roy-bastion"
  instance_count         = 1

  ami                    = "ami-0652eb0db9b20aeaf"
  instance_type          = "t2.micro"
  key_name               = "roy-import"
  monitoring             = true
  vpc_security_group_ids = ["${module.my_vpc.default_security_group_id}","${module.security-group.this_security_group_id}"]
  # 用到两个security group,都引入默认的那个,可以保证VCP内部的机器能互联。
  subnet_id              = "${module.my_vpc.public_subnets[0]}"

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

module "appsg" {
  source = "terraform-aws-modules/security-group/aws"
  name        = "app-service"
  description = "Security group for App within VPC"
  vpc_id      = "${module.my_vpc.vpc_id}"


  ingress_with_source_security_group_id = [
    {
    rule                     = "all-all"
    source_security_group_id = "${module.security-group.this_security_group_id}"
    },
  ]

  egress_with_source_security_group_id = [
    {
    rule                     = "all-all"
    source_security_group_id = "${module.security-group.this_security_group_id}"
    },
  ]
}
module "my_backend" {
  source                 = "terraform-aws-modules/ec2-instance/aws"
  version                = "~> 2.0"

  name                   = "roy-backend"
  instance_count         = 2

  ami                    = "ami-0652eb0db9b20aeaf"
  instance_type          = "t2.micro"
  key_name               = "roy-import"
  monitoring             = true
  vpc_security_group_ids = ["${module.my_vpc.default_security_group_id}","${module.appsg.this_security_group_id}"]
  subnet_id              = "${module.my_vpc.private_subnets[0]}"

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

2. Azure例子

这个例子是在Azure 环境创建 vnet 和 subnet,本来可以写得很简单,通过例子理清思路,把逻辑链条搞清楚。

2.1 目录结构:

├── dev
│   └── test.tf
└─── modules
    ├── subnet
    │   ├── output.tf
    │   ├── subnet.tf
    │   └── variables.tf
    └── vnet
        ├── output.tf
        ├── variables.tf
        └── vnet.tf

说明:

modules目录下面两个子目录代表了两个模块,output.tf 和 variables.tf 用于输出和输入。dev目录是代表了实例,实现模块调用;还可以创建多个目录,代表多个实例。

2.2 模块文件介绍

2.2.1 vnet部分

# file vnet/vnet.tf

resource "azurerm_resource_group" "core" {
  name     = "${var.cluster_prefix}-rg"
  location = "${var.cluster_location}"
}

resource "azurerm_virtual_network" "core" {
  name                = "${var.cluster_prefix}-vnet"
  address_space       = ["${var.cluster_cidr}"]
  location            = "${azurerm_resource_group.core.location}"
  resource_group_name = "${azurerm_resource_group.core.name}"
}

这里面创建了 resource group 和 virtual network。里面涉及到了变量,创建下面的文件来定义它们。

# file vnet/variables.tf

variable "cluster_prefix" {
  type        = "string"
  description = "The name of the cluster."
}

variable "cluster_location" {
  type        = "string"
  description = "The Azure region to use https://azure.microsoft.com/en-us/regions/"
}

variable "cluster_cidr" {
  type        = "string"
  description = "The IP range that will be used for the virtual network."
}

有一些结果在之后的模块中要调用,这就需要在当前模块输出。

# file vnet/output.tf

output "cluster_resource_group" {
  value = "${azurerm_resource_group.core.name}"
}

output "virtual_network" {
  value = "${azurerm_virtual_network.core.name}"
}

output "cluster_location" {
  value = "${var.cluster_location}"
}

output "cluster_cidr" {
  value = "${var.cluster_cidr}"
}

2.2.2 subnet部分

# file subnet/subnet.tf
resource "azurerm_subnet" "current" {
  name                 = "${var.subnet_name}"
  resource_group_name  = "${var.cluster_resource_group}"
  virtual_network_name = "${var.virtual_network}"
  address_prefix       = "${var.subnet_cidr}"
}

这里面又出现了 resource group 和 vnet,这在上一个模块中出现过,从这个角度来说,应该把它合并到上一个模块,减少重复的代码。这个例子是为了演示过程,忽略优化问题,在下面文件中重复定义变量。

# file subnet/variables.tf

variable "cluster_resource_group" {}
variable "virtual_network" {}
variable "subnet_name" {}
variable "subnet_cidr" {}

【注意】变量中的 cluster_resource_group 和 virtual_network 是上一个模块的 output。也就是说变量是通过 output 来跨模块传递的。

2.3 模块调用

# file dev/test.tf

module "my_vnet" {
  source           = "../modules/vnet"
  cluster_prefix   = "roytest"
  cluster_location = "eastasia"
  cluster_cidr     = "10.0.0.0/16"
}

module "my_subnet" {
  source                 = "../modules/subnet"
  subnet_name            = "bastion"
  cluster_resource_group = "${module.my_vnet.cluster_resource_group}"
  virtual_network        = "${module.my_vnet.virtual_network}"
  subnet_cidr            = "10.0.1.0/24"
}

这个文件就是调用了两个模块,source 定义了从哪里调用模块,把变量输入进去。需要注意的是第二个模块,变量里面需要输入前一个模块的输出结果,它的调用格式是

  cluster_resource_group = "${module.my_vnet.cluster_resource_group}"
  virtual_network        = "${module.my_vnet.virtual_network}"

${module.my_vnet.virtual_network} ,module 表示从模块里面获取,my_vnet 是实例的名字(不是模块的名字vnet),virtual_network 这个名字要与模块中的output输出相匹配。

前面是大量的铺垫,这一部分才是核心内容,表明了跨模块怎么实现调用的。

2.4 验证

$ cd dev
$ terraform init
....
$ terraform apply --auto-approve

3. 改造自己的 module

从官网 Registry 找到对应模块,确定对应 github 地址,克隆下来,修改对应的内容,于是就成了自定义模块。

涉及到的核心知识点在上面的例子中已经说明,具体改造情况(略)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容