自動化編排編排工具 Terrafor

【導讀】本文介紹了可以編排雲原生資源的 Terraform。

Terraform 是一個 IT 基礎架構自動化編排工具,它的口號是 "Write, Plan, and create Infrastructure as Code", 基礎架構即代碼。Terraform 幾乎可以支持所有市面上能見到的雲服務。

Terraform 要解決的就是在雲上那些硬件資源分配管理的問題。相比較 Chef, Puppet, Ansible 這些軟件配置工具,Terraform 提供的是軟件配置之前,軟硬件(基礎)資源構建的問題。

當我們創建資源時,使用 terraform 比 ansible 好在哪裏?

就創建資源這個角度來說,terraform 和 ansible 都能完成,terraform 能夠併發,效率高很多,另外它在資源生產成功之後會在本地以一個 state 文件的形式記錄整個資源的詳細信息,而這些信息的記錄使得整個模板所定義的資源可以保證前後端的高度一致性,可以有利於後續對於整個一套資源的有效的版本控制。同時 Terraform 擁有一個 Data Source 功能,利用這個功能可以實現對於已有資源的獲取,比如在生產資源之前想要查看當前有哪些可用區,有哪些可用鏡像等,所有的這些都可以通過 DataSource 實現。

Terraform 和 Ansible 的結合

安裝軟件

  1. 下載 zip 文件 https://www.terraform.io/downloads.html

  2. 解壓後直接就能用。把文件放到合適的路徑,比如 /usr/local/bin

生成配置文件

  1. 新建目錄,並生成配置文件,比如 azure.tf
# Configure the provider
provider "azurerm" {
   version = "=1.20.0"
}

# Create a new resource group
resource "azurerm_resource_group" "rg" {
   name     = "royTR"
   location = "eastasia"
}

配置有兩部分:provider 和 resource。provider 告知與哪一個雲平臺打交道,這裏是 Azure;如果使用 AWS,這裏就寫成 provider "aws"。第二部分是資源,說明要生成哪些資源,例子中是 resource group,還可以繼續往下寫,比如網卡,存儲,虛擬機等。

格式:resource resource_type resource_name { }

A resource block has two string parameters before opening the block: the resource type (first parameter) and the resource name (second parameter). The combination of the type and name must be unique in the configuration.

我已經通過 Azure CLI 登陸過,所以上面 provider 部分沒有提供用戶驗證信息,如果單獨配置,使用如下形式:

# Configure the Microsoft Azure Provider
provider "azurerm" {
  # More information on the authentication methods supported by
  # the AzureRM Provider can be found here:
  # http://terraform.io/docs/providers/azurerm/index.html

  subscription_id = "..."
  client_id       = "..."
  client_secret   = "..."
  tenant_id       = "..."
}

這些信息怎麼獲取? 可以用 Azure CLI 的命令生成:

az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/${SUBSCRIPTION_ID}"

詳細信息參考微軟文檔

創建資源

  1. 初始化

在初始化項目的時候,Terraform 會解析目錄下的 *.tf 文件並加載相關的 provider 插件。

$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "azurerm" (1.20.0)...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
  1. apply changes

This output shows the execution plan, describing which actions Terraform will take in order to change real infrastructure to match the configuration.

$ terraform apply .

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
 + create

Terraform will perform the following actions:

 + azurerm_resource_group.rg
     id:       <computed>
     location: "eastasia"
     name:     "royTR"
     tags.%:   <computed>

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: yes  # 查看 execution plan 符合期望,輸入 yes 確認,之後真正執行。

azurerm_resource_group.rg: Creating... location: ""=>"eastasia"name:"" => "royTR" tags.%: ""=>"" azurerm_resource_group.rg: Creation complete after 1s (ID: /subscriptions/7c91db0e-eb7f-491b-997f-32cf55b85dea/resourceGroups/royTR)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

6. 查看狀態

```shell
$ terraform state show
id       = /subscriptions/7c91db0e-eb7f-491b-997f-32cf55b85dea/resourceGroups/royTR
location = eastasia
name     = royTR
tags.%   = 0

更多

$ terraform state list
module.roy-azure.azurerm_availability_set.hdp-avset
module.roy-azure.azurerm_network_interface.bastion-nic
...
$ terraform state show module.roy-azure.azurerm_virtual_machine.hdp-slave[1]
...
location                                                         = japaneast
name                                                             = roy-tf0-hdp-slave-02
...
$ terraform state show module.roy-azure.azurerm_network_interface.hdp[0]
...
ip_configuration.0.load_balancer_backend_address_pools_ids.#       = 0
ip_configuration.0.load_balancer_inbound_nat_rules_ids.#           = 0
ip_configuration.0.name                                            = hdp-01-ip-conf
....
private_ip_address                                                 = 10.0.10.8
...

更改資源

  1. 改配置

修改剛纔的文件,添加 tag 部分。

   # Configure the provider
   provider "azurerm" {
       version = "=1.20.0"
   }

   # Create a new resource group
   resource "azurerm_resource_group" "rg" {
       name     = "royTR"
       location = "eastasia"
       tags {
           environment = "TF sandbox"
       }
   }
  1. apply changes
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
 ~ update in-place

Terraform will perform the following actions:

 ~ azurerm_resource_group.rg
     tags.%:           "0" ="1"
     tags.environment: "" ="TF sandbox"

Plan: 0 to add, 1 to change, 0 to destroy.

## 銷燬基礎設施

> terraform destroy

```shell
$ terraform destroy
azurerm_resource_group.rg: Refreshing state... (ID: /subscriptions/xxxx/resourceGroups/royTR-rg)

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy

Terraform will perform the following actions:

- azurerm_resource_group.rg


Plan: 0 to add, 0 to change, 1 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

azurerm_resource_group.rg: Destroying... (ID: /subscriptions/xxxxx/resourceGroups/royTR-rg)
azurerm_resource_group.rg: Still destroying... (ID: /subscriptions/xxxx/resourceGroups/royTR-rg, 10s elapsed)
azurerm_resource_group.rg: Still destroying... (ID: /subscriptions/xxxxx/resourceGroups/royTR-rg, 20s elapsed)
azurerm_resource_group.rg: Still destroying... (ID: /subscriptions/xxxxx/resourceGroups/royTR-rg, 30s elapsed)
azurerm_resource_group.rg: Still destroying... (ID: /subscriptions/xxxxx/resourceGroups/royTR-rg, 40s elapsed)
azurerm_resource_group.rg: Destruction complete after 48s

Destroy complete! Resources: 1 destroyed.

單獨刪除一個資源:

$ terraform destroy -target=module.roy-azure.azurerm_virtual_machine.hdp[2]
...
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  - module.roy-azure.azurerm_virtual_machine.hdp[2]


Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
....
Destroy complete! Resources: 1 destroyed.

資源的依賴關係

要創建一個 VM,需要一些資源已經具備,這些資源可能包括:

先來一個簡單的例子,創建網絡:

# Create virtual network
resource "azurerm_virtual_network" "vnet" {
    name                = "royTFVnet"
    address_space       = ["10.0.0.0/16"]
    location            = "${azurerm_resource_group.rg.location}"
    resource_group_name = "${azurerm_resource_group.rg.name}"
}

location等部分引入了插值(interpolation),它已經在前面的資源定義,之後直接調用,格式是 TYPE.NAME.ATTRIBUTE.
Azure 網絡和虛擬機的基礎架構如下圖所示:

azure_vm_structure.png

把上面的圖,變成代碼,創建 VM 需要的整個文件:

# Configure the provider
provider "azurerm" {
    version = "=1.20.0"
}

# Create a new resource group
resource "azurerm_resource_group" "rg" {
    name     = "royTR"
    location = "eastasia"
    tags {
        environment = "TF sandbox"
    }
}

# Create virtual network
resource "azurerm_virtual_network" "vnet" {
    name                = "royTFVnet"
    address_space       = ["10.0.0.0/16"]
    location            = "${azurerm_resource_group.rg.location}"
    resource_group_name = "${azurerm_resource_group.rg.name}"
}

# Create subnet
resource "azurerm_subnet" "subnet" {
    name                 = "royTFSubnet"
    resource_group_name  = "${azurerm_resource_group.rg.name}"
    virtual_network_name = "${azurerm_virtual_network.vnet.name}"
    address_prefix       = "10.0.1.0/24"
    #address_prefix       = "${cidrsubnet(var.cluster_cidr, 8, 10)}"
}

# Create public IP
resource "azurerm_public_ip" "publicip" {
    name                         = "myTFPublicIP"
    location                     = "${azurerm_resource_group.rg.location}"
    resource_group_name          = "${azurerm_resource_group.rg.name}"
    public_ip_address_allocation = "dynamic"
    }

# Create Network Security Group and rule
resource "azurerm_network_security_group" "nsg" {
    name                = "myTFNSG"
    location            = "${azurerm_resource_group.rg.location}"
    resource_group_name = "${azurerm_resource_group.rg.name}"

    security_rule {
        name                       = "SSH"
        priority                   = 1001
        direction                  = "Inbound"
        access                     = "Allow"
        protocol                   = "Tcp"
        source_port_range          = "*"
        destination_port_range     = "22"
        source_address_prefix      = "*"
        destination_address_prefix = "*"
    }
}

# Create network interface
resource "azurerm_network_interface" "nic" {
    name                      = "myNIC"
    location                  = "${azurerm_resource_group.rg.location}"
    resource_group_name       = "${azurerm_resource_group.rg.name}"
    network_security_group_id = "${azurerm_network_security_group.nsg.id}"

    ip_configuration {
        name                          = "myNICConfg"
        subnet_id                     = "${azurerm_subnet.subnet.id}"
        private_ip_address_allocation = "dynamic"
        public_ip_address_id          = "${azurerm_public_ip.publicip.id}"
    }
}

# Create a Linux virtual machine
resource "azurerm_virtual_machine" "vm" {
    name                  = "royTFVM"
    location              = "${azurerm_resource_group.rg.location}"
    resource_group_name   = "${azurerm_resource_group.rg.name}"
    network_interface_ids = ["${azurerm_network_interface.nic.id}"]
    vm_size               = "Standard_DS1_v2"

    storage_os_disk {
        name              = "myOsDisk"
        caching           = "ReadWrite"
        create_option     = "FromImage"
        managed_disk_type = "Premium_LRS"
    }

    storage_image_reference {
        publisher = "Canonical"
        offer     = "UbuntuServer"
        sku       = "16.04.0-LTS"
        version   = "latest"
    }

    os_profile {
        computer_name  = "royvm"
        admin_username = "royzeng"
    }

    os_profile_linux_config {
        disable_password_authentication = true
        ssh_keys {
            path     = "/home/royzeng/.ssh/authorized_keys"
            key_data = "ssh-rsa AAAAB3Nz{snip}hwhqT9h"
        }
    }

}

使用 Provisioners 進行環境配置

Provisioners 可以在資源創建 / 銷燬時在本地 / 遠程執行腳本。

Provisioners 通常用來引導一個資源,在銷燬資源前完成清理工作,進行配置管理等。

Provisioners 擁有多種類型可以滿足多種需求,如:文件傳輸(file),本地執行(local-exec),遠程執行(remote-exec)等 Provisioners 可以添加在任何的 resource 當中:

# Create a Linux virtual machine
resource "azurerm_virtual_machine" "vm" {

<...snip...>

    provisioner "file" {
        connection {
            type        = "ssh"
            user        = "royzeng"
            private_key = "${file("~/.ssh/id_rsa")}"
        }

        source      = "newfile.txt"
        destination = "newfile.txt"
    }

    provisioner "remote-exec" {
        connection {
            type        = "ssh"
            user        = "royzeng"
            private_key = "${file("~/.ssh/id_rsa")}"
        }

        inline = [
        "ls -a",
        "cat newfile.txt"
        ]
    }

}

上面的方式適合有 public ip,能夠直接連接的機器,對於不能直接連接的 vm,通過跳板來實現。

官方的方法,定義 bastion_host

resource "null_resource" "connect_private" {
  connection {
    bastion_host = "${aws_instance.bastion.public_ip}"
    host         = "${aws_instance.private.private_ip}"
    user         = "ubuntu"
    private_key  = "${file("~/.ssh/id_rsa")}"
  }

  provisioner "remote-exec" {
    inline = ["echo 'CONNECTED to PRIVATE!'"]
  }
}

或者

resource "azurerm_virtual_machine" "vm" {

<...snip...>


    provisioner "remote-exec" {
        connection {
            bastion_host= "${azurerm_public_ip.bastion.ip_address}"
            type        = "ssh"
            user        = "${var.admin-username}"
            private_key = "${file("~/.ssh/id_rsa")}"
        }

        inline = [
        "sudo parted /dev/disk/azure/scsi1/lun0 mklabel msdos",
        "sudo parted /dev/disk/azure/scsi1/lun0 mkpart primary 1 100%",
        "sudo partprobe",
        "sleep 5; sudo mkfs.xfs /dev/disk/azure/scsi1/lun0-part1",
        "sudo mkdir /roytest",
        "sudo mount /dev/disk/azure/scsi1/lun0-part1 ${var.mount_path[0]}",
        "echo 'UUID='`sudo blkid -s UUID -o value $(readlink -f /dev/disk/azure/scsi1/lun0-part1)`  ${var.mount_path[0]} 'xfs defaults  0 0' | sudo tee -a /etc/fstab",
        "df -hl | grep /dev/sd"
        ]
    }
}

另一種方法,用 local-exec 來跳轉

  provisioner "local-exec" {
    ## 簡化方式
    command = "ssh -o "ProxyCommand ssh -q -W %h:%p -i mykey jump_server” -C 'echo hello'"
    ## 真實環境用的方式
    command = <<EOF
        sleep 30; ansible-playbook -i '${element(azurerm_network_interface.master_bind.*.private_ip_address, count.index)},' ${local.ansible_ssh_args} ${var.ansible_path}/mount_disk.yml --extra-vars '{
        "root_user": "centos",
        "deviceName": "/dev/disk/azure/scsi1/lun0",
        "mountPath": "${var.mount_path}",
        "bind_zone_name": "${var.bind_zone_name}"
        }'
        EOF
    }

使用 null resource 和 trigger 來解耦

爲了讓 ansible 腳本單獨運行,而不需要創建或銷燬資源,可以用 null_resource 調用 provisioner 來實現。

resource "null_resource" "datanode" {
  count = "${var.count.datanode}"

  triggers {
    instance_ids = "${element(aws_instance.datanode.*.id, count.index)}"
  }

  provisioner "remote-exec" {
    inline = [
      ...
    ]

    connection {
      type = "ssh"
      user = "centos"
      host = "${element(aws_instance.datanode.*.private_ip, count.index)}"
    }
  }
}

輸入變量

新建一個文件定義變量

# file variables.tf
---
variable "prefix" {
  default = "royTF"
}

variable "location" { }

variable "tags" {
  type    = "map"
  default = {
     Environment = "royDemo"
     Dept = "Engineering"
  }
}

文件中 location 部分沒有定義,運行 terraform 的時候,會提示輸入:

$ terraform plan -out royplan
var.location
  Enter a value: eastasia

  <...snip...>

  This plan was saved to: royplan

To perform exactly these actions, run the following command to apply:
    terraform apply "royplan"

其它輸入變量的方式

命令行輸入

$ terraform apply \
>> -var 'prefix=tf' \
>> -var 'location=eastasia'

文件輸入

$ terraform apply \
  -var-file='secret.tfvars'

默認讀取文件 terraform.tfvars,這個文件不需要單獨指定。

環境變量輸入

TF_VAR_name ,比如 TF_VAR_location

變量類型

對於 list 變量

# 定義 list 變量
variable "image-RHEL" {
  type = "list"
  default = ["RedHat""RHEL""7.5""latest"]
}

# 調用 list 變量

    storage_image_reference {
        publisher = "${var.image-RHEL[0]}"
        offer     = "${var.image-RHEL[1]}"
        sku       = "${var.image-RHEL[2]}"
        version   = "${var.image-RHEL[3]}"
    }

map 是一個可以被查詢的表。

variable "sku" {
    type = "map"
    default = {
        westus = "16.04-LTS"
        eastus = "18.04-LTS"
    }
}

查詢方式 (使用 lookup)

storage_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "${lookup(var.sku, var.location)}"
    version   = "latest"
}

輸出變量

定義輸出

output "ip" {
    value = "${azurerm_public_ip.publicip.ip_address}"
}

測試

$ terraform apply
...

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

  ip = 52.184.97.1
$ terraform output ip
52.184.97.1

Bug? 第一次運行,ip 輸出是空的,terraform output ip 命令的結果也是空的,過一段時間才能看到結果。

$ terraform output -module=roy-azure
bastion-private-ip = 10.0.1.4
bastion-public-ip = 40.115.243.72
cluster_cidr = 10.0.0.0/16
cluster_location = japaneast
cluster_prefix = roy-tf0
cluster_resource_group = roy-tf0-rg
hdp-master-ip = 10.0.10.4,10.0.10.6,10.0.10.7
hdp-master-name = roy-tf0-hdp-master-01,roy-tf0-hdp-master-02,roy-tf0-hdp-master-03
hdp-slave-ip = 10.0.10.5,10.0.10.9,10.0.10.8
hdp-slave-name = roy-tf0-hdp-slave-01,roy-tf0-hdp-slave-02,roy-tf0-hdp-slave-03
k8s-master-ip = 10.0.20.8,10.0.20.5
k8s-master-name = roy-tf0-k8s-master-01,roy-tf0-k8s-master-02
k8s-slave-ip = 10.0.20.6,10.0.20.7,10.0.20.4
k8s-slave-name = roy-tf0-k8s-slave-01,roy-tf0-k8s-slave-02,roy-tf0-k8s-slave-03
virtual_network = roy-tf0-vnet

Data Source

DataSource 的作用可以通過輸入一個資源的變量名,然後獲得這個變量的其他屬性字段。

用 Azure 網絡 來舉例,提供一些信息,查詢其它的屬性。具體必須提供什麼,能查到什麼,參考這個鏈接。

data "azurerm_virtual_network" "test" {
  name                = "production"
  resource_group_name = "networking"
}

output "virtual_network_id" {
  value = "${data.azurerm_virtual_network.test.id}"
}

output "virtual_network_subnet" {
  value = "${data.azurerm_virtual_network.test.subnets[0]}"
}

生成主機列表

ansible 通過主機列表來連接目標主機,我們就要想辦法讓 terraform 來生成。(local-exec 也是一種方式,這是另一種思路:用 terraform 調用 ansible)

terraform 生成 inventory 的思路是:從模板到文件,需要先用 template_file 渲染成一個字符串,然後用 local_file 把這個字符串輸出到一個文件。

模版文件

## file inventory.tpl

[backend]
${bastion_private_ip}

[frontend]
${bastion_pub_ip}

[all:vars]
ansible_ssh_private_key_file = ${key_path}
ansible_ssh_user = dcpuser

渲染和輸出

## file inventory.tf

data "template_file" "inventory" {
    template = "${file("./test/inventory.tpl")}"

    vars {
        bastion_private_ip      = "${element(azurerm_network_interface.bastion-nic.*.private_ip_address, count.index)}"
        bastion_pub_ip          = "${element(azurerm_public_ip.bastion.*.ip_address, count.index)}"
        key_path = "~/.ssh/id_rsa"
    }
}

resource "local_file" "save_inventory" {
  content  = "${data.template_file.inventory.rendered}"
  filename = "./myhost"
}

運行後,當前目錄生成文件 myhost

[backend]
13.78.94.242

[frontend]
10.0.1.4

[all:vars]
ansible_ssh_private_key_file = ~/.ssh/id_rsa
ansible_ssh_user = dcpuser

對於多個主機,使用 join 來把它們合在一起。

File inventory.tf

data  "template_file" "k8s" {
    template = "${file("./templates/k8s.tpl")}"
    vars {
        k8s_master_name = "${join("\n", azurerm_virtual_machine.k8s-master.*.name)}"
    }
}

resource "local_file" "k8s_file" {
  content  = "${data.template_file.k8s.rendered}"
  filename = "./inventory/k8s-host"
}

File k8s.tpl

[kube-master]
${k8s_master_name}

Final result

[kube-master]
k8s-master-01
k8s-master-02
k8s-master-03

使用 module 進行代碼的組織管理

Module 是 Terraform 爲了管理單元化資源而設計的,是子節點,子資源,子架構模板的整合和抽象。將多種可以複用的資源定義爲一個 module,通過對 module 的管理簡化模板的架構,降低模板管理的複雜度,這就是 module 的作用。

Terraform 中的模塊是以組的形式管理不同的 Terraform 配置。模塊用於在 Terraform 中創建可重用組件,以及用於基本代碼組織。每一個 module 都可以定義自己的 input 與 output,方便代碼進行模塊化組織。

用模塊,可以寫更少的代碼。比如用下面的代碼,調用已有的 module 創建 vm。

調用官方 module

# declare variables and defaults
variable "location" {}
variable "environment" {
    default = "dev"
}
variable "vm_size" {
    default = {
        "dev"   = "Standard_B2s"
        "prod"  = "Standard_D2s_v3"
    }
}

# Use the network module to create a resource group, vnet and subnet
module "network" {
    source              = "Azure/network/azurerm"
    version             = "2.0.0"
    location            = "${var.location}"
    resource_group_name = "roytest-rg"
    address_space       = "10.0.0.0/16"
    subnet_names        = ["mySubnet"]
    subnet_prefixes     = ["10.0.1.0/24"]
}

# Use the compute module to create the VM
module "compute" {
    source            = "Azure/compute/azurerm"
    version           = "1.2.0"
    location          = "${var.location}"
    resource_group_name = "roytest-rg"
    vnet_subnet_id    = "${element(module.network.vnet_subnets, 0)}"
    admin_username    = "royzeng"
    admin_password    = "Password1234!"
    remote_port       = "22"
    vm_os_simple      = "UbuntuServer"
    vm_size           = "${lookup(var.vm_size, var.environment)}"
    public_ip_dns     = ["roydns"]
}

調用自己寫的 module

## file main.cf

module "roy-azure" {
  source = "./test"
}

## file test/resource.tf

variable "cluster_prefix" {
  type        = "string"
}
variable "cluster_location" {
    type        = "string"
}

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

轉自:

jianshu.com/p/e0dd50f7ee98

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BPdS66qMgeAfy62Wj46Deg