大家好,本文是《搭建简易家庭IT实验室》系列文章的第三篇,描述了如何使用一个在本地搭建一个简易的Terraform来创建及销毁我的PVE中的虚拟机。所有内容并非面向Linux大佬,而是面对像我一样不会Linux但是又喜欢玩数字化过家家的朋友。本文记录了我学习相关知识的过程,也供感兴趣的朋友参考。有什么不对或者可以做得更好的地方,还请大家不吝赐教。
阅读本文需要您知晓虚拟化的基本概念,建议先阅读上一篇:《搭建家庭简易IT实验室——cloud-init》。
C 前情提要
在上一篇中,我们使用PVE自带的命令qm
以及红帽的工具cloud-init
做到了命令行创建并销毁虚拟机。我们可以很快速滴得到若干虚拟机,它们都具有相同的设置,主机名等个性化设置除外。为了管理虚拟机,我们要么是使用web控制台鼠标点点点,要么是shell命令行敲敲敲。想象下需要大量虚拟机的时候,我们这样重复劳动,肝疼不疼?而且是人工操作就有可能出错,有的错误后果不堪设想;还有就是开发人员测试App的时候想要退回“上月那个环境”,您就:“???上哪找去?”
这一篇我们将学到如何使用Terraform(以下简称tf)达成以上目标,tf是HashiCorp公司的一个自动化工具,可以管理各种公有云私有云,得益于网上社区的力量,tf几乎可以管理各种IT基础架构。
“基础架构即代码”(IaC)使用代码来管理和配置IT基础架构。相对于古法手作,IaC省去了很多步骤和时间,可以与代码版本控制集成,还能减少对工作人员的技能需求,确保工作质量,堪称CI/CD必玩必会的工具。注:我们在这里是用tf管理PVE的虚拟机,重要的是思路,我相信看完了这文章您也可以用tf创建一个AWS的VPC。
IaC有两种实施方法:声明式(declarative)和命令式(imperative)。tf正是使用声明式IaC的一种工具软件。tf一次部署/配置通常需要三个阶段:
- Write——用户需要用HCL (HashiCorp Configuration Language)编写声明文件。
- Plan——用户在正式制备/改变配置之前有机会预览效果
- Apply——用户梦想成真
以上看着眼熟吧?我抄袭我自己。
Dm 安装
tf运行在Linux/macOS/Windows上,也不需要什么服务什么进程守护。以Debian为例:
1
2
3
4
5
6
| sudo apt install wget gpg -y
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/terraform-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/terraform-archive-keyring.gpg] https://apt.releases.hashicorp.com bullseye main" | sudo tee /etc/apt/sources.list.d/terraform.list
sudo apt update
sudo apt install terraform -y
terraform -v
|
我自己用的是Ansible,对应上面步骤写了个简单的playbook,分享在这里:
感兴趣的点开复制保存,第一个冒号后面是要运行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
| - hosts: <要运行TF的机器名字>
become: true
become_method: sudo
tasks:
- name: Install gpg
apt:
name: gpg
state: latest
update_cache: yes
- name: Remove gpg keys
file:
path: "{{ item }}"
state: absent
with_items:
- /usr/share/keyrings/hashicorp-archive-keyring.gpg_armored
- /usr/share/keyrings/hashicorp-archive-keyring.gpg
- name: Download HashiCorp gpg key
get_url:
url: https://apt.releases.hashicorp.com/gpg
dest: /usr/share/keyrings/hashicorp-archive-keyring.gpg_armored
checksum: sha256:ecc3a34eca4ba12166b58820fd8a71e8f6cc0166d7ed7598a63453648f49c4c5
- name: De-Armor HashiCorp gpg key
shell: gpg --dearmor < /usr/share/keyrings/hashicorp-archive-keyring.gpg_armored > /usr/share/keyrings/hashicorp-archive-keyring.gpg
no_log: true
args:
creates: /usr/share/keyrings/hashicorp-archive-keyring.gpg
- name: Add HashiCorp's repository to APT sources list
apt_repository:
repo: "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com {{ ansible_distribution_release }} stable"
filename: /etc/apt/sources.list.d/hashicorp.list
state: present
update_cache: true
- name: Install TF
apt:
name: terraform
state: latest
update_cache: no
|
Em PVE端的准备
为了让tf能与PVE交互,我们得先在PVE上创建一个用户,这个用户应该有能完成工作所需的最小权限。
先加个角色并给予权限:
1
| sudo pveum role add terraform -privs "VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Monitor VM.Audit VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit"
|
我也不知道需要多少权限,看名字瞎猜的。先点这么多,不够再加。
建个用户:
1
| sudo pveum user add terraform-svc@pam
|
给这个用户赋予角色:
1
| sudo pveum aclmod / -user terraform-svc@pam -role terraform
|
给这个用户创建访问REST API的凭据:
1
| sudo pveum user token add terraform-svc@pam terraform-token --privsep=0
|
以上会返回这样的信息,记下来,并且保密,至少不能像我这样公开发出来:
1
2
3
4
5
6
7
8
9
| ┌──────────────┬──────────────────────────────────────┐
│ key │ value │
╞══════════════╪══════════════════════════════════════╡
│ full-tokenid │ terraform-svc@pam!terraform-token │
├──────────────┼──────────────────────────────────────┤
│ info │ {"privsep":"0"} │
├──────────────┼──────────────────────────────────────┤
│ value │ c2cf68f4-eadd-41c8-a743-375bc1a3042b │
└──────────────┴──────────────────────────────────────┘
|
然后可以试试这套凭据,不一定要从pve服务器上运行:
1
2
| curl "https://<pve服务器以及端口>/api2/json/access/permissions" \
-H 'Authorization: PVEAPIToken=terraform-svc@pam!terraform-token=c2cf68f4-eadd-41c8-a743-375bc1a3042b'
|
注:使用自签名证书的也许需要–insecure
选项。
tf运行的时候默认会在当前文件夹寻找几个文件,作为信息的输入。默认情况下它还会在当前文件夹用文件来保存状态和锁。
以下是一个典型的文件夹(不含状态和锁):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| .
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│ ├── foo/
│ │ ├── README.md
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ ├── bar/
│ ├── .../
|
我们从空白开始:
1
| mkdir ~/terraform-pve && cd ~/terraform-pve
|
main.tf
这是最主要的入口,甚至可以是唯一的入口,不过不推荐这样做。main.tf
可以包含描述管理资源的代码,在复杂一点的项目中,后者应该与main.tf
分割,独立存放。我的main.tf
是这样的:
1
2
3
4
5
6
7
8
9
10
| terraform {
required_providers {
proxmox = {
source = "telmate/proxmox"
}
dns = {
source = "hashicorp/dns"
}
}
}
|
其中描述了我们将使用两个插件,具体内容为双引号包围的部分。注意我不用DNS插件,这里只是为了演示调用超过一个插件的写法。
provider.tf
这个也可以不要,内容写在上面的文件里也行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| provider "proxmox" {
# pm_tls_insecure = true
pm_api_url = var.pm_api_url
pm_api_token_id = var.pm_api_token_id
pm_api_token_secret = var.pm_api_token_secret
}
provider "dns" {
update {
server = var.dns_ip
key_name = var.dns_key
key_algorithm = "hmac-md5"
key_secret = var.dns_key_secret
}
}
|
里面内容可以用变量,也可以写死。
variables.tf
各位当然知道,大多数情况下写死并不明智,于是我们用一个variables.tf
来定义变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| ...
variable "pm_agent_enabled" {
description = "Enable Proxmox agent (qemu-quest-agent daemon must be installed/running on the guest)"
type = number
default = 1
}
variable "pm_api_url" {
description = "Proxmox API endpoint URL"
type = string
sensitive = true
}
variable "pm_api_token_id" {
description = "Proxmox API token ID"
sensitive = true
}
variable "pm_api_token_secret" {
description = "Proxmox API token secret"
sensitive = true
}
...
|
我这个情景中,上面的type
可以不写,而sensitive
则必须写,tf看到这种变量不会将其值输出。
.tfvars或者环境变量
用了变量就得有地方给变量赋值。
可以写在.tfvars
文件里,但是强烈不建议!
可以写在.tfvars
文件里,但是强烈不建议!
可以写在.tfvars
文件里,但是强烈不建议!
一个例子:k8s-prod.tfvars
:
1
2
3
4
5
6
| dns_ip = "192.168.1.103"
dns_key = "labDomainKey"
dns_key_secret = "bGFiRG9tYWluS2V5Cg=="
pm_api_url = "https://<pve服务器以及端口>/api2/json"
pm_api_token_id = "terraform-svc@pam!terraform-token"
pm_api_token_secret = "c2cf68f4-eadd-41c8-a743-375bc1a3042b"
|
想必各位一眼就能看出这么写的缺点,凑合用用还行,稍微正经点的地方都不会这么写。稍微好点的方法是弄成环境变量。在运行tf的工作站上运行:
1
2
3
| export TF_VAR_pm_api_url=https://<pve服务器以及端口>/api2/json
export TF_VAR_pm_api_token_id=terraform-svc@pam!terraform-token
export TF_VAR_pm_api_token_secret=c2cf68f4-eadd-41c8-a743-375bc1a3042b
|
不过这样做仍然有泄露的风险,比如history
命令或者printenv
就能出卖一切。不过比前面那种写在文件里好多了。追求更安全的同学可以去搜索“key vault”关键词,HashiCorp/Azure/Oracle都有解决方案。我这里不涉及——因为我不会——这边最终运行tf是在用过即销毁的工作站 (CI/CD runner)上。
<module.tf>
前面铺垫那么多,终于要做事了。我们建个文件,比如k8s-prod.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
| terraform {
required_providers {
proxmox = {
source = "telmate/proxmox"
}
}
}
resource "proxmox_vm_qemu" "pve-tf" {
count = <要几台就写几>
agent = var.pm_agent_enabled
vmid = 201 + count.index
name = "k8s-prod-0${1 + count.index}"
target_node = var.pm_target_node
clone = var.pm_vm_template
full_clone = var.pm_vm_full_clone
os_type = var.pm_vm_os_type
cores = var.pm_vm_cores
sockets = var.pm_vm_sockets
cpu = var.pm_vm_cpu_type
memory = var.pm_vm_memory
scsihw = var.pm_vm_scsihw
bootdisk = var.pm_vm_bootdisk
disk {
size = var.pm_vm_disk_size
type = var.pm_vm_disk_type
storage = var.pm_vm_disk_storage
#storage_type = "lvmthin"
iothread = var.pm_vm_disk_iothread
}
network {
model = var.pm_vm_network_model
bridge = var.pm_vm_network_bridge
}
lifecycle {
ignore_changes = [
network,
]
}
# Create Ansible user, introduce its SSH key pub.
ciuser = var.pm_vm_ciuser
sshkeys = var.pm_vm_sshkeys
}
|
以上文件有些不用写,因为默认值就正好可以用,这里为了演示所以把大部分都写出来了。具体见文档。我们约定:虚拟机的id从201开始,名字从k8s-prod-01开始,IP地址用DHCP。其实这里应该用个IPAM/DNS的API来搞,比如Infoblox,因为这个要钱,我又找不到免费的平替,就先偷懒用DHCP了。
注意每一个变量都要在上上节的variables.tf
里面定义,上上节我只写了4个作为示例。
tf还会有输出文件(默认无)和状态文件,状态文件主要是能让tf知道工作进度及状况,默认存在当前文件夹。
G 运行
先初始化一下,让tf把插件什么的准备好,此时仍然可以介绍变量给tf:
1
2
3
| terraform init
或者
terraform init -k="v"
|
从这时起,状态文件和锁就生成了。
然后可以预览下,同样可以加变量:
确定可以做了就执行,还是可以加变量:
虚拟机等亿小会就可以用了。
不想用的时候就
Am 总结
这篇文章我们使用tf去访问PVE的REST API以管理虚拟机,设置好变量等输入文件后,即可高效滴制备或者摧毁虚拟机,但是仍然有可以改进的地方。比如状态文件terraform.tfstate
存放在本地磁盘,多台工作站的情景中就不太方便了。下一篇我们将把它挪到云存储去。
好了,看过就等于会了。谢谢观看。