这是开源项terraform-azure-demo的官方技术文档。

开门见山,阅读本文后,你将学会如何使用terraform工具配合HCL语言完成AzureCloud资源配置和构建。

如果你还不了解如何在项目中真正的使用Azure云,那就耐心的读下去吧,这里浓缩了一个实际项目的方方面面。

项目传送门 > terraform-azure-demo

你既然已经找到了这里,肯定是有需求或者想要学习在AzureCloud上自动构建资源的技术,那么我们先来看看这个项目简介和概览。

项目简介

terraform是由Hashicorp公司开源出来的,用于云服务资源部署、规划和管理的工具集。它理论上可以服务于任何云服务商,包括你所知道的云服务几大巨头,如:微软云(AzureCloud),亚马逊(AWS)等。

本项目主要基于AzureCloud,使用terraform工具来规划和管理AzureCloud上的资源,并构建可用的实用案例。

该项目真实有效,你几乎只需要修改一小部分,就可以应用到项目中。

项目概览

要想尽快的熟悉一个项目,做一个整体的把握是必不可少的,文档的说明就是这个作用,不管是什么样的开源项目,只要它开源出来肯定是希望大家一起能够学习并充实它,让它更富有生命力。

本项目是一个简单的开源项目,只是一个笔者在工作中浓缩的经验集合,看起来短小的它其实很精悍,毫不夸张的说,如果你完全明白该项目的所有细节,开发一个实际项目是没有什么问题的。

书归正传,为了让大家有一个更好的理解,笔者打算用框图来表示一个项目的概况,配合着一些简单的讲解,更有助于你快速掌握。

项目概览图

基本上所有的内容都有在图上标注,这里强调一些重要概念,可以让你更清楚我们的意图:

  • 顶层模块(Top-Level)是真实的用户可配置的交互模块,但任然可以被用于其他模块的子模块(submodule)
  • 模块的定义就和你熟悉的函数一样,它具有输入(input variable)和输出(output)
  • 模块的参数传递都是经过Input variable来完成的,并且和函数式编程类似,它们支持默认值,也支持覆写
  • 模块的输出都是通过Output来完成的,其他的模块组件内容,对外都不可见
  • terraform默认支持远程模块的能力,这就表示我们可以很轻易的使用类似github等远程代码托管仓库中的代码,复用更方便
  • terrform配置文件扩展名为.tf,它还有一些其他的文件定义形式,当然也会有不同的作用,项目中使用的*.auto.tfvars就是其中一种,它的作用是覆写顶层模块的参数,其他格式请自行查询terrform的官方文档
  • terraform定义模块是和目录结构有关的,一个模块基本上都在同一个目录层级,使用构建指令的时候,并不会在没有引用子模块的时候递归查询子目录

项目仓库目录树:

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
prexer $ tree
.
├── backend.tf
├── configs
│   └── hibro-azure.tmpl.yml
├── default.auto.tfvars
├── demo-steps
│   ├── demo.session
│   └── timing.steps
├── LICENSE
├── main.tf
├── Makefile
├── modules
│   ├── moduleDisk
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── README.md
│   │   └── variables.tf
│   └── moduleNet
│   ├── main.tf
│   ├── outputs.tf
│   ├── README.md
│   └── variables.tf
├── outputs.tf
├── README.md
├── statefile
└── variables.tf

6 directories, 19 files

当前复现环境

截止到现在,这个项目是有效的,后续随着Azure和Terraform项目进行迭代,也许会存在版本兼容的问题,不过在可见的未来,应该大体不会变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
prexer $ terraform --version
Terraform v0.13.5
+ provider registry.terraform.io/hashicorp/azurerm v2.34.0
+ provider registry.terraform.io/hashicorp/null v3.0.0
+ provider registry.terraform.io/hashicorp/random v3.0.0
+ provider registry.terraform.io/hashicorp/template v2.2.0

prexer $ cat /proc/version
Linux version 4.15.0-1098-azure (buildd@lcy01-amd64-022) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)) #109~16.04.1-Ubuntu SMP Wed Sep 30 18:53:14 UTC 2020

prexer $ az version
{
"azure-cli": "2.14.0",
"azure-cli-core": "2.14.0",
"azure-cli-telemetry": "1.0.6",
"extensions": {
"ai-examples": "0.2.4"
}
}

快速上手尝鲜

如果想要使用这个Demo项目,你最少应该进行如下步骤:

  1. 注册一个Azure云账号,可以自己建立或者通过公司的管理者进行开通
  2. 让你的账户所属一个可控的订阅中,一般都是公司或其他组着管控
  3. 获得适当的权限,至少拥有可以创建一定范围内资源的权限
  4. 最后,就是熟悉如何使用Azure门户和Bash Shell

不过,不管怎么样,微软云都提供了一个免费体验的服务,你可以在微软进行认证后,获得200美金为期一个月的使用权,用来做前期的设计和实践已经足够了。

然后,登录Azure打开Cloud Bash Shell,然后运行(这是一整个资源的生命周期,按照自己的需求使用相应的指令)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 创建一个临时目录
mkdir demo && cd $_

# 2. 克隆demo示例,并跳转工作目录
git clone https://github.com/prexerx/terraform-azure-demo.git && cd terraform-azure-demo

# 3. 初始化terraform转态文件和一些脚本插件资源
make init # or terraform init

# 4. 制定计划,一定要仔细看
make plan t=demo

# 5. 应用计划,跟踪输出
make apply t=demo

# 6. 制定删除计划,请仔细查看每一个细节,避免造成不可恢复的损失
make dplan t=demo

# 7. 慎重执行删除操作,除非你知道自己在做什么,否则不要使用
make dapply t=demo

如果你没有功夫注册微软云服务,那你可以通过笔者的示例演示,来回放我在云端操作的细节,复现步骤就是简单的运行:

1
2
# [from] script -t 2> timing.steps -a demo.session
scriptreplay demo-steps/timing.steps demo-steps/demo.session

它会给你展示一个完整的终端交互过程,和你在云端的操作如出一辙。

快速获取技术帮助

基本上如果你有一定的编程经验,那完全可以自给自足的完成本Demo的自主构建。

在寻求帮助的时候,搜索引擎是必不可少的,使用起来你应该已经得心应手了,就像这样:

如果你不懂resource "azurerm_resource_group",那么通过搜索引擎来按照关键字查询,如:

1
2
3
> resource "azurerm_resource_group" site:terraform.io
# or
> terraform resource "azurerm_resource_group"

详细的定向技术文档会马上呈现在你的面前,你只需要挑选你需要的部分,细心研读即可。

项目实现细节

整体把握后,我们来看看具体的实现细节。

由于我们是基于AzureCloud来配置资源,导致文中不可避免的会多一些AzureCloud的相关说明,不过本着够用即最好的原则,要想使用本项目,只需要了解本文讲解的内容即可。

为了更好的理解项目内容,模块的划分和文件重心也经过精心的设计,后续实现的说明我们按照指定文件来一步步说明。

由于terraform是基于HCL语言来进行解释资源的,所以你可以先看看官方的语法支持,了解一个大概,基本上这个语言是很好理解的,都是关键字和语句块组成,并配合着命名空间来实现资源的抽象和划分。

好了,开始看看源码的细节吧!

backend.tf

backend.tf文件是给terraform工具本身添加自定义配置项的。

1
2
3
4
5
6
7
8
9
10
11
12
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 2.26"
}
}
}

provider "azurerm" {
features {}
}

这里告诉terraform,我们的后端打算用AzureCloud,在terraform规划的资源管理方面,AzureCloud名称为azurerm也就是Azure Resource Management,并定义了工具的版本参数,它会影响工具的插件集合,所以最好指定一个高阶版本;至于features是AzureCloud等运营商特定的内容,基本上实践项目也没有特别的指定,但又必须实现这个块来使用默认值,所以放在这里也无伤大雅。

接着迎来了重头戏:

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
# for Azure backend, you can store your statefile on Azure Cloud.

# terraform {
# backend "azurerm" {
# resource_group_name = "Azure resource group name"
# storage_account_name = "Storage Account in your resource group"
# container_name = "You should generate a container in your Storage Account, such as: terraform-state"
# key = "Terraform Statfile Name, such as: name.terraform.tfstate"
# access_key = "Storage Account Access Key, you can get it from Azure portal."
# }
# }

# for reference from other resources.

# data "terraform_remote_state" "hibro" {
# backend = "azurerm"

# config = {
# storage_account_name = "same as backend azurerm block"
# container_name = "same as backend azurerm block"
# key = "same as backend azurerm block"
# access_key = "same as backend azurerm block"
# }
# }

# Just to teach you with local backend.
terraform {
backend "local" {
path = "statefile/hibro.terraform.tfstate"
}
}

# for refer local statefile.

# data "terraform_remote_state" "hibro_state" {
# backend = "local"

# config = {
# path = "${path.module}/statefile/hibro.terraform.tfstate"
# }
# }

关于terraform.backend技术块,它是用来说明terraform的状态文件(名称类似*.terraform.tfstate)将要存储在哪里,也就是说在执行terraform initterraform apply指令的时候,前者会找到存储位置然后进行后续资源的判定,而后者也是由于前面资源的判定,进而决定哪些资源需要创建、修改或者删除等操作。

后端存储基本分为两种:

  1. 本地存储,这也是默认行为,如果你不配置它会存储在你本地的当前工作目录
  2. 远程存储,你可以根据需求存储terraform状态文件到远程位置,如:微软的存储账户,或者github等地方。

我们为了方便演示,将状态文件存储在本地指定的目录位置:statefile/(相对目录),并且命名为hibro.terraform.tfstate

存储在远程的位置也是很简单的,如图所示,获取远程资源后,一次填写配置即可:(我们假设你已经构建了一个存储账户)

Azure上获得配置

至于container_name需要在Azure上创建一个,当然Azure提供了az指令可以完成这个工作,不过我们演示下从web界面操作的方法:

在存储账户上创建容器

至于剩下的data.terraform_remote_state块,是作引用你存储过的状态文件之用;也就是说在引用之前,你的文件一定是存在的,当然它也对应着远程和本地位置,如果状态文件不存在,terraform会报错提示你找不到相应的状态文件。

对于backend = "azurerm"/"local"引用块来说,这部分说明你要引用的后端位置,其他的配置基本上和前文配置的存储位置一致。

你也不用诧异,很多时候,这个状态文件是作为terraform的枢纽一样的存在,它可以帮助你存贮资源的概况,甚至作为其他资源的输入部分,只要引用它,就可以得到它的所有输出。

default.auto.tfvars

前文已经提到,terraform支持很多扩展文件名,如.tf,.auto.tfvars等,其他格式去terraform官网看看吧,这里就不展开介绍了。

对于本项目中的default.auto.tfvars文件来说,它的作用是覆写默认的顶层模块配置参数值。

首先你必须知道,在default.auto.tfvars文件中配置的值,一定要在variable.tf文件中也定义,否则你去覆写谁呢?而且按照前文的描述,模块的输入都是通过input variable来完成的,对应的部分就是variable.tf文件中的内容。

这里再说明下,一个模块(限定一个模块)的所有资源都可以写到一个文件中,就算写到不同的tf文件中,最后terraform也会整合到一起;terraform的做法是,解析当前模块目录中的所有tf文件,并不递归解析;不过对于复杂的项目来说,根据文件来区分重要的部分,可以更清晰的了解项目细节,而且这也是terraform官方推荐的做法。

文件中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
prefix   = "hibro"
location = "westus2"

os_publisher = "kinvolk"
os_offer = "flatcar-container-linux-free"
os_sku = "stable"
os_version = "latest"

admin_username = "core"
admin_password = "disable for flatcar OS"
admin_pub_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCorWm8FA4ovqbqWGQgoQ2QJo+CuFK7Dr8Zujen6iQVCWaWx/2oCGOi4sgpwAt9H5zfXFBag7eXrG9lL4DE3W5GONmcdn4v8fA0AASORu++mUfLBN3YjcQAudRQLlOtVYVaHTRbL2jiXBHuNvZjkrH1EyCKPdAZEPWauGOE4CXtE/e0Qlcb9i/rK7Eqm3b7/BzbUiNUJv1XpLFuJFSR5YugnSzBxkghvsyz4oOsbJ65pwPgwCGI1gsECcQ3WN93REwekOOedIONRlEzbp6KCCWWAf9rFD4E++INQAvmB+Js8X1WxWydeq3NWHMmFNDLRqAuUnyvsVzVwwBSdlNPJVzb"

这和你们接触过的,如定义变量,出奇的一致,不是么?只不过terraform支持的类型基本和python或者json类似,这里也不过多说明,后续我们会用到字符串、布尔变量、列表、字典和对象等类型。

variable.tf(顶层输入)

这里我们就顶层模块的输入部分来说明,它的官方说法是input variable,不过本文对于input variablevariable并不区分。

由于篇幅限制,我们只讲解重点说明的部分,截取部分代码,后续也是如此,如果要看详细配置,请自行克隆项目源码,它并不多,不是么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
variable "admin_username" {
type = string
description = "Administrator user name for virtual machine"
default = "hibro"
}
...
## a map for tags
variable "tags" {
type = map
default = {
hibroTags = "Terraform Demo with Tags"
}
}
...

这里仅仅列举两种变量类型,字符串和字典,当然布尔值、列表和其他类型也很简单,自行查询文档就可以了。

对于variable.xxx块来说,后面的xxx部分就是变量的名字,也是别人引用的变量名称,不过这里有一点需要注意,以往的引用都是一个完整的命名空间前缀,如:data.terraform.backend这样,而对于变量的引用却有些不同,应该使用var.xxx来完成。

type < 变量中可以存在类型的强定义,如果没有定义那么terraform会自行推导。

description < 变量的解释说明,由于这是一门DSL,所以它的解释说明对于一个资源来说是有需要的,但不是必要的

default < 变量的默认值,如果没有传入变量值,则会使用默认值,如果没有默认值又没有传入变量,则会报错

type = map定义一个字典变量,就是你理解的键值对,并没有什么特别,嵌套和对象的引用和你熟悉的语言类似

main.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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207

# Create a Top Level Resource Group 怎么看都是注释,不是么?
# 定义一个资源组,名称为"rg",引用名称为'azurerm_resource_group.rg'
# 这里和常用的命名空间也有所不同,不用带有'resource'关键字
resource "azurerm_resource_group" "rg" {
name = "${var.prefix}-RG" # 资源组的名字
# 资源组所在位置,也就是说你的资源组应该建立在哪里,如美国西部(westus)或者欧洲(europe)等
# 同样的,它会关联你的其他资源,他们需要放在一起,跨域很多时候是不行的!
location = var.location
}

# Generate random text for a unique storage account name
# terraform内置的资源,可以帮忙随机产生一个随机8位ID,用于后续的存贮账户命名
resource "random_id" "randomId" {
keepers = { # 持有者为前文定义的资源组
resource_group = azurerm_resource_group.rg.name
}
# 随机字符数目
byte_length = 8
}

# Create storage account for boot diagnostics
# 创建一个存储账户,名称为sa
resource "azurerm_storage_account" "sa" {
# 存储账户的名字,它必须是AzureCloud上唯一的,所以使用了一个字符串拼接的技巧,并且内部扩展了变量,引用了之前生成的随机ID
name = "hibro${random_id.randomId.hex}"
# 所属资源组,后续不再说明
resource_group_name = azurerm_resource_group.rg.name
# 资源所处位置,后续不再说明
location = var.location
# 存储账户的天梯榜,也就是性能,分为:Standard and Premium
account_tier = "Standard"
# 定义用于此存储帐户的复制类型,也是默认值
account_replication_type = "LRS"
# 自定义的资源标签,用于处理对象关系,这里的类型是一个字典
tags = {
uAwesomeAccount = "hibro-Account"
}
}

# 引用一个submodule,注意,这不是定义,而是引用模块的写法
module "hibroNet" { # 引用模块后,在本模块中定义了一个别名,为`module.hibroNet`
# 引用外部子模块的语法,这是引用本地模块的语法,解析了相对于当前模块位置的其他模块,如 moduleNet
source = "./modules/moduleNet"

# 模块变量的参数传递,有时候我们会根据自己的需要修改引用模块的参数,这里就是一个方案
# 需要注意的是,这里的参数定义和引用模块的参数定义是对应的,子模块必须定义这些变量才行!
prefix = var.prefix # 一个项目特定的前缀,后文都是一样的`hibro-`
location = azurerm_resource_group.rg.location
resource_group = azurerm_resource_group.rg.name
}

module "hibroDisk" {
source = "./modules/moduleDisk" # 引用本地子模块moduleDisk

prefix = var.prefix
location = azurerm_resource_group.rg.location
resource_group = azurerm_resource_group.rg.name
}

module "hibroRemote" {
# 引用远程子模块,它位于github上,在terraform init的时候会导入它到本地,本质上和本地差不多。
# 语法形式为官方提供,只需要按照标准书写即可
source = "git::https://github.com/prexerx/terraform-remote-module.git"

prefix = var.prefix
location = azurerm_resource_group.rg.location
resource_group = azurerm_resource_group.rg.name
# 这里的技巧和其他的不同,这里使用了前文的`module.hibroNet`模块的输出,并用它的ID来作为远程模块的输入
# 默认的,它也就说明了一种依赖关系,说明远程模块(hibroRemote)资源的构建需要依赖本地模块(hibroNet)的资源,先后顺序一目了然
subnet_id = module.hibroNet.subnet_id # 这也是一种对象类型

depends_on = [ # 如果没有显示的资源依赖关系定义,那么就需要使用这个关键字段来说明,这是一个列表对象,用逗号分隔所依赖的资源
azurerm_resource_group.rg
]
}

# Create a Linux virtual machine
# 创建一个虚拟机,有很多的官方资源关键字可以使用,不过我们就以`azurerm_virtual_machine`说明,其他都类似
resource "azurerm_virtual_machine" "vm" { # 虚拟机资源定义别名
name = "hibro-VM" # 虚拟机资源的名字
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [module.hibroRemote.nic_id] # 附着在虚拟机上的网卡列表,一台机器多个网卡很常见不是么?
primary_network_interface_id = module.hibroRemote.nic_id # 主网卡标记

vm_size = var.vm_size # 这很重要,表示你使用了一个什么性能虚拟机,它包括类似CPU,内存,缓存等相关的资源,计费也是比较可观的,所以请根据自己的需求,查看官方文档后来选择

# 当删除虚拟机的时候也删除系统盘
delete_os_disk_on_termination = true

boot_diagnostics { # 调试块,还有很多参数,不过我们只关心虚拟机开机启动的调试log
enabled = var.debug_enable # 使能调试
# 指定log的存储位置,这里存储在我们前文创建的存储账户中了,所有的开机log都在那里!
storage_uri = azurerm_storage_account.sa.primary_blob_endpoint
}

storage_os_disk { # 构建虚拟机操作系统镜像的磁盘
name = "${var.prefix}-OsDisk" # 系统盘的名字
caching = "ReadWrite" # 缓存形式,这些概念需要你有一些操作系统方面的知识
create_option = "FromImage" # 从ISO创建,就这样,目前Flatcar也通常是使用这种方法来配置的,AzureCloud会帮你拉取ISO镜像
managed_disk_type = "Premium_LRS" # 磁盘的性能,也是查表获取的内容,关键字直接查询即可,而且你还会看到不同的价格表
}

storage_image_reference { # 操作系统的引用说明,它目前需要配合plan来说明
publisher = var.os_publisher # Flatcar的发布者
offer = var.os_offer # Flatcar的提供方
sku = var.os_sku # 版本属性,如stable,next等
version = var.os_version # 版本导航,如latest或者2516.2等
}

plan { # 目前,我们配合前文storage_image_reference来说明计划,配置内容和前面一样,否则会报错
name = var.os_sku
publisher = var.os_publisher
product = var.os_offer
}

storage_data_disk { # 挂载外部磁盘
# 引用了本地模块hibroDisk的属性,如名字和ID,还有后文的磁盘大小
name = module.hibroDisk.disk_touchMeStorage_name
managed_disk_id = module.hibroDisk.disk_touchMeStorage_id
create_option = "Attach" # 挂接
lun = 0 # 磁盘在VM中的唯一编号
disk_size_gb = module.hibroDisk.disk_touchMeStorage_size
caching = "ReadOnly" # 磁盘的缓存属性为只读,但不是磁盘只读,说明缓存不能被动态写入而已
}

storage_data_disk {
name = module.hibroDisk.disk_hitMeStorage_name
managed_disk_id = module.hibroDisk.disk_hitMeStorage_id
create_option = "Attach"
lun = 1 # 磁盘在VM中的唯一编号
disk_size_gb = module.hibroDisk.disk_hitMeStorage_size
caching = "ReadWrite" # 另一种缓存属性,它说明可以支持动态修改缓存,不过对于系统来说只有操作系统视图有用
}

os_profile { # 操作系统的规划配置
computer_name = "hibro-VM" # 主机名
admin_username = var.admin_username # 有管理权限的用户名
# admin_password = var.admin_password # (login with ssh public key) # 定义密码
# 配置文件渲染后的输出,这里需要重点说明下
# 这里引用了后文的配置文件对象,并且它会把渲染后的文件放到VM中的指定位置:/var/lib/waagent/CustomData
# 这里有一个[0],说明资源hibro_config是有count属性的,索引从0开始
custom_data = data.template_file.hibro_config[0].rendered
}

os_profile_linux_config { # 这是类似的扩展配置内容,看起来是后续加的,不过我们只关心内容就可以
# disable_password_authentication = false # (login with ssh public key) # 如果配置密码,那后面的内容就需要注释掉
disable_password_authentication = true # 不使用密码,由于flatcar的特殊性,请使用SSH Key来登陆管理用户
ssh_keys {
# 配置VM中存储你要传入的SSH pub key的地方
path = "/home/${var.admin_username}/.ssh/authorized_keys"
# 你传入的SSH pub key值,用对应的私钥解密,对么?
key_data = var.admin_pub_key
}
}

tags = var.tags # 直接应用map类型变量的例子
}

# terraform官方提供的渲染文件的模板关键字`template_file`
data "template_file" "hibro_config" {
count = 1 # 定义了一个count属性,后续医用资源的时候从0开始
# 模板文件的位置,file是terraform内置的函数获得文件,`path.module`为内置的变量,指定当前模块的目录位置
template = file("${path.module}/configs/hibro-azure.tmpl.yml")

vars = { # 渲染文件中的变量替换,好奇么?看看`configs/hibro-azure.tmpl.yml`文件你就明白了
hibro_vm_ip_address = module.hibroRemote.public_ip_address
others_environment = "hibro hacker"
}
}

# 这依旧是terraform定义的一个内置资源类型,它和其他的资源一样,只不过没有实体对应,就进行一些操作而已。
# 这里通过这个资源来对已经启动的VM进行一些操作
resource "null_resource" "hibro_config_update" {

triggers = {
# 定义一个触发资源更新的条件,这是对于`terraform状态文件`使用的而言的,如果文件更新了,资源才会再次操作
template_rendered = data.template_file.hibro_config[0].rendered
}

connection { # 定义连接到虚拟机的方式
type = "ssh" # 通过该ssh协议
user = "core" # 用户名,这里之所以没有写变量是因为提醒用户,对于Flatcar而言core通常就是默认的用户
# Azure CloudShell Must use Public IP Address to access the VM.
# 这里比较有意思,后面会详细的说明,这里你只需要知道是对一个远程模块定义的Public IP的引用即可
host = module.hibroRemote.public_ip_address
# Azure CloudShell SSH Private Key location!
# Azure CloudShell的私钥存放位置,对应的肯定是公钥
private_key = file("~/.ssh/id_rsa")
}

provisioner "file" { # 就是说明要干什么,file表示创建一个文件
# 内容是前文的渲染后版本
content = data.template_file.hibro_config[0].rendered
# 文件的写入位置和文件名称
destination = "/tmp/hibro-update"
}

provisioner "remote-exec" { # remote-exec 说明要执行一些指令,就是你认为的常见的linux指令
inline = [ # 指定的指令集合,以列表的形式说明
"whoami && ls /tmp/ -l"
]
}
# 需要虚拟机已经创建好才能进行操作,不是么?
depends_on = [azurerm_virtual_machine.vm]
}

此项目的所有资源都是统一前缀为hibro-,这只是一个自定义设计而已,但对于隔绝其他的命名空间,简单且有效。

模块在引用时,都会带着命名空间前缀module,类似module.xxx.xxx

资源的创建有一定的依赖关系,terraform可以自行推导显示指定的依赖关系,不过如果没有显示指定的资源又产生了依赖,那么在并行创建的时候就会出现问题,所以有时候我们需要使用depends_on字段来说明资源的依赖关系。

拥有count属性的资源,是允许用户创建多个类似资源的快捷方式,引用的时候需要一些额外技巧,通过[0]的方式来引用,count定义的时候是从1开始,而引用的时候从0开始。

Terraform内置的函数和变量需要参考官方的技术手册,走你

需要知道的是,Azure CloudShell也是Azure给你提供的一个ubuntu虚拟机的bash shell伪终端而已,它也依附于一个系统之上,并没有多么神秘。

配置中有一项host = module.hibroRemote.public_ip_address要说明下,默认情况下Azure Cloudshell访问Azure自建虚拟机是有限制的,首先如果你的Azure Cloudshell没有和自建虚拟机在同一个网段,那么他们不能彼此访问,如果需要通过Azure CloudShell访问虚拟机,那就需要VM有一个挂接的公网IP,之后通过公网IP就可以通讯,当然Azure也提供了将Azure Cloud Shell并入你公司内网的方案,请自行查看官方文档以寻求帮助。

还有一点请注意,在Azure虚拟机中想要通过指令ping来测试连通性,比如从Azure VM到Azure CloudShell ubuntu是不行的,Azure官方有对ICMP协议的限制。

output.tf(顶层输出)

对于顶层模块的输出,你可以灵活的进行配置,各种资源的输出你都可以尝试,我们的Demo只提供了一个条欢迎信息和一个有用的自动构建的虚拟机公网IP地址,如果你要使用当前登录,只需要知道公钥/私钥如何匹配,然后运行:ssh core@<vm_public_ip>即可完成登录。

1
2
3
4
5
6
7
# Top-Level Output
output "Hi_Bro_From_Author" {
value = "Nice to meet you, any question, file to < prexer.163.com > | .. ^_^ .. "
}
output "hibro_vm_public_ip" {
value = module.hibroRemote.public_ip_address
}

子模块的讲解部分

后文是对于子模块的讲解部分,每个模块内容比较少,所以我们把一个模块作为一个整体来阐述细节。

moduleDisk

模块的内容如下,我们依旧通过注释详解的方式来讲述案例

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
# variable.tf
# 这里承接顶层模块的参数传递,如果不传入,就会报错!
variable "prefix" {
type = string
}
variable "location" {
type = string
}
variable "resource_group" {
type = string
}

# main.tf
# 定义一个磁盘资源
resource "azurerm_managed_disk" "touchMeStorage" {
name = "${var.prefix}-touchMe-storage" # 磁盘资源的名字
location = var.location
resource_group_name = var.resource_group
storage_account_type = "Premium_LRS" # 磁盘的性能指标,查看Azure官网就可以了
create_option = "Copy" # 这里说明从其他磁盘拷贝一个完整的镜像到自己身上
# 只有当`create_option`为`Copy`的时候才会有这个属性,它说明从哪里拷贝数据
source_resource_id = azurerm_managed_disk.copyMeStorage.id
disk_size_gb = "8" # 磁盘的大小,单位我就不用细说了吧...
}

resource "azurerm_managed_disk" "hitMeStorage" {
#count = 1 # 磁盘可以有多个分身,不是么?批量的话,可以试试count属性
name = "${var.prefix}-hitMe-Storage"
location = var.location
resource_group_name = var.resource_group
storage_account_type = "StandardSSD_LRS"
create_option = "Empty" # 这个属性说明磁盘建立的时候是空的,也说明它的建立非常快速,不是么?
disk_size_gb = "8"
}

# no use, just to be copied, don't export out.
# 这里也有注释说明,这个磁盘资源并没有被使用,而是作为一个替身,被别人复制了!其他的并没有什么特殊
resource "azurerm_managed_disk" "copyMeStorage" {
name = "${var.prefix}-copyMe-Storage"
location = var.location
resource_group_name = var.resource_group
storage_account_type = "Premium_LRS"
create_option = "Empty"
disk_size_gb = "8"
}

# output.tf
# 这部分是该子模块的输出,而顶层模块也只能引用它的输出部分,并不能看见它的内部结构。
# 引用的时候应该类似:module.<alias_name>.disk_touchMeStorage_name
output "disk_touchMeStorage_name" {
value = azurerm_managed_disk.touchMeStorage.name
}

output "disk_touchMeStorage_id" {
value = azurerm_managed_disk.touchMeStorage.id
}

output "disk_touchMeStorage_size"{
value = azurerm_managed_disk.touchMeStorage.disk_size_gb
}

output "disk_hitMeStorage_name" {
value = azurerm_managed_disk.hitMeStorage.name
}

output "disk_hitMeStorage_id" {
value = azurerm_managed_disk.hitMeStorage.id
}

output "disk_hitMeStorage_size"{
value = azurerm_managed_disk.hitMeStorage.disk_size_gb
}

moduleNet

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
# variable.tf
# 必要的传入参数,你懂的
variable "prefix" {
type = string
}
variable "location" {
type = string
}
variable "resource_group" {
type = string
}

# main.tf
# 网络资源部分的讲解在本文不会涉猎太多,因为如果要说明它,可能要一本书才能完成,请自行查阅资料。
# 当然必要的简单描述还是有必要的!
# Create virtual network
# 定义一个虚拟网络,网络拓扑应该是这样的关系
# vnet -> subnet(with mask) -> private ip
resource "azurerm_virtual_network" "vnet" {
name = "${var.prefix}-Vnet"
address_space = ["10.0.0.0/16"] # 虚拟网络的网段定义
location = var.location # 这里强调下网络的配置,之前提到过跨域的资源访问,网络资源是绝对不能跨地域来访问的,不然要VPN干嘛?
resource_group_name = var.resource_group
}

# Create subnet
# 定义一个子网网段
resource "azurerm_subnet" "subnet" {
name = "${var.prefix}-Subnet"
resource_group_name = var.resource_group
virtual_network_name = azurerm_virtual_network.vnet.name # 所属的虚拟网络,正好是我们前面创建额
address_prefixes = ["10.0.1.0/24"] # 子网网段,`10.0.1.0`是符合私用标准的范围,免费内网使用
}

# Create Network Security Group and rule
# 定义一个网络安全组,它其实是说明为了网络安全,哪些端口,或者协议可以开放,进行更细粒度的限制
resource "azurerm_network_security_group" "nsg" {
name = "${var.prefix}-NSG"
location = var.location
resource_group_name = var.resource_group

security_rule { # 定义一个安全规则
name = "SSH" # 开通SSH协议
priority = 1001 # 优先级,当不同协议被定义的时候,数据被截获的顺序和优先级有关
direction = "Inbound" # 输入方向
access = "Allow"
protocol = "Tcp" # 传输层协议
source_port_range = "*" # 所有地址都可以访问
destination_port_range = "22" # SSH标准端口
source_address_prefix = "*" # 没有地址前缀限制
destination_address_prefix = "*" # 目标地址也没哟前缀限制
}
}

# output.tf
# 这是网络模块的输出部分,它对外给虚拟机的网卡输出一个`SUBNET ID`,来让网卡可以挂到我们创建的子网上面。
output "subnet_id" {
value = azurerm_subnet.subnet.id
}

moduleRemote

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
# variable.tf
variable "prefix" {
type = string
}
variable "location" {
type = string
}
variable "resource_group" {
type = string
}
# 由于这个模块我们要绑定网卡到子网上,所以需要传入外部已经定义好的子网ID
variable "subnet_id" {
type = string
}

# main.tf
# Create public IP
# 申请一个public ip资源
resource "azurerm_public_ip" "publicip" {
name = "${var.prefix}-PublicIP"
location = var.location
resource_group_name = var.resource_group
allocation_method = "Static" # 分配一个静态IP地址,只有两种类型`dynamic`和`static`
}
# 当外部想要引用资源的未导出数据时,需要定义一个同名的data资源块,然后完成引用
# 需要知道,有些资源的数据在没有导出的时候是不能引用的,也就是你看不见,这种方法也是官方推荐的!
# 你需要的只是定义一个同名块,然后填写必要的数据即可。
data "azurerm_public_ip" "publicip" {
name = azurerm_public_ip.publicip.name
resource_group_name = azurerm_public_ip.publicip.resource_group_name
}

# Create network interface
# 定义一个网卡
resource "azurerm_network_interface" "nic" {
name = "${var.prefix}-NIC"
location = var.location
resource_group_name = var.resource_group

ip_configuration { # 网卡配置参数
name = "${var.prefix}-NIC-Confg"
subnet_id = var.subnet_id # 所属子网
private_ip_address_allocation = "dynamic" # 在子网中,被分配IP的类型,这里是动态的
public_ip_address_id = azurerm_public_ip.publicip.id # 关联的公网IP,也就是前文创建的Public IP资源
}
}

# output.tf
# 为顶层模块的虚拟机提供网卡支持,输出ID即可
output "nic_id" {
value = azurerm_network_interface.nic.id
}
# 为顶层模块提供自动构建的公网IP,用于顶层模块的输出显示!
output "public_ip_address" {
value = data.azurerm_public_ip.publicip.ip_address
}

有好的建议和想法?联系我吧

本项目的官方文档会持续更新,如果有什么建议可以直接邮件给作者prexer@163.com