前言

当我们成功地执行了一次terraform apply,创建了期望的基础设施以后,我们如果再次执行terraform apply,生成的新的执行计划将不会包含任何变更,Terraform会记住当前基础设施的状态,并将之与代码所描述的期望状态进行比对。第二次apply时,因为当前状态已经与代码描述的状态一致了,所以会生成一个空的执行计划

概念

1.状态文件

Terraform引入了一个独特的概念——状态管理,这是Ansible等配置管理工具或是自研工具调用SDK操作基础设施的方案所没有的。简单来说,Terraform将每次执行基础设施变更操作时的状态信息保存在一个状态文件中,默认情况下会保存在当前工作目录下的terraform.tfstate文件里。例如我们在代码中声明一个data和一个resource

data "ucloud_images" "default" {
  availability_zone = "cn-sh2-01"
  name_regex        = "^CentOS 6.5 64"
  image_type        = "base"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
  name = "my-vpc"
}

使用terraform apply后,我们可以看到terraform.tfstate的内容

{
  "version": 4,
  "terraform_version": "0.13.5",
  "serial": 54,
  "lineage": "a0d89a84-ae5b-8e14-d61b-2d9885e3359a",
  "outputs": {},
  "resources": [
    {
      "mode": "data",
      "type": "ucloud_images",
      "name": "default",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "availability_zone": "cn-sh2-01",
            "id": "1693951353",
            "ids": [
              "uimage-xiucsl"
            ],
            "image_id": null,
            "image_type": "base",
            "images": [
              {
                "availability_zone": "cn-sh2-01",
                "create_time": "2020-01-09T11:30:34+08:00",
                "description": "",
                "features": [
                  "NetEnhanced",
                  "CloudInit"
                ],
                "id": "uimage-xiucsl",
                "name": "CentOS 6.5 64位",
                "os_name": "CentOS 6.5 64位",
                "os_type": "linux",
                "size": 20,
                "status": "Available",
                "type": "base"
              }
            ],
            "most_recent": false,
            "name_regex": "^CentOS 6.5 64",
            "os_type": null,
            "output_file": null,
            "total_count": 1
          }
        }
      ]
    },
    {
      "mode": "managed",
      "type": "ucloud_vpc",
      "name": "vpc",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "cidr_blocks": [
              "10.0.0.0/16"
            ],
            "create_time": "2020-11-16T17:00:40+08:00",
            "id": "uvnet-lu2vcdds",
            "name": "my-vpc",
            "network_info": [
              {
                "cidr_block": "10.0.0.0/16"
              }
            ],
            "remark": null,
            "tag": "Default",
            "update_time": "2020-11-16T17:00:40+08:00"
          },
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

我们可以看到,查询到的data以及创建的resource信息都被以json格式保存在tfstate文件里。

我们前面已经说过,由于tfstate文件的存在,我们在terraform apply之后立即再次apply是不会执行任何变更的,那么如果我们删除了这个tfstate文件,然后再执行apply会发生什么呢?Terraform读取不到tfstate文件,会认为这是我们第一次创建这组资源,所以它会再一次创建代码中描述的所有资源。更加麻烦的是,由于我们前一次创建的资源所对应的状态信息被我们删除了,所以我们再也无法通过执行terraform destroy来销毁和回收这些资源,实际上产生了资源泄漏。所以妥善保存这个状态文件是非常重要的。

另外,如果我们对Terraform的代码进行了一些修改,导致生成的执行计划将会改变状态,那么在实际执行变更之前,Terraform会复制一份当前的tfstate文件到同路径下的terraform.tfstate.backup中,以防止由于各种意外导致的tfstate损毁。

在Terraform发展的极早期,HashiCorp曾经尝试过无状态文件的方案,也就是在执行Terraform变更计划时,给所有涉及到的资源都打上特定的tag,在下次执行变更时,先通过tag读取相关资源来重建状态信息。但因为并不是所有资源都支持打tag,也不是所有公有云都支持多tag,所以Terraform最终决定用状态文件方案。

还有一点,HashiCorp官方从未公开过tfstate的格式,也就是说,HashiCorp保留随时修改tfstate格式的权力。所以不要试图手动或是用自研代码去修改tfstate,Terraform命令行工具提供了相关的指令(我们后续会介绍到),请确保只通过命令行的指令操作状态文件

2.警惕: tfstate是明文的

关于Terraform状态,还有极其重要的事,所有考虑在生产环境使用Terraform的人都必须格外小心并再三警惕:Terraform的状态文件是明文的,这就意味着代码中所使用的一切机密信息都将以明文的形式保存在状态文件里。例如我们回到创建UCloud主机的例子:

data "ucloud_security_groups" "default" {
  type = "recommend_web"
}

data "ucloud_images" "default" {
  availability_zone = "cn-sh2-02"
  name_regex        = "^CentOS 6.5 64"
  image_type        = "base"
}

resource "ucloud_instance" "normal" {
  availability_zone = "cn-sh2-02"
  image_id          = data.ucloud_images.default.images[0].id
  instance_type     = "n-basic-2"
  root_password     = "supersecret1234"
  name              = "tf-example-normal-instance"
  tag               = "tf-example"
  boot_disk_type    = "cloud_ssd"
  security_group = data.ucloud_security_groups.default.security_groups[0].id
  delete_disks_with_instance = true
}

我们在代码中明文传入了root_password的值是supersecret1234,执行了terraform apply后我们观察tfstate文件中相关段落:

{
      "mode": "managed",
      "type": "ucloud_instance",
      "name": "normal",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allow_stopping_for_update": null,
            "auto_renew": false,
            "availability_zone": "cn-sh2-02",
            "boot_disk_size": 20,
            "boot_disk_type": "cloud_ssd",
            "charge_type": null,
            "cpu": 2,
            "cpu_platform": "Intel/Broadwell",
            "create_time": "2020-11-16T18:06:32+08:00",
            "data_disk_size": null,
            "data_disk_type": null,
            "data_disks": [],
            "delete_disks_with_instance": true,
            "disk_set": [
              {
                "id": "bsi-krv0ilrc",
                "is_boot": true,
                "size": 20,
                "type": "cloud_ssd"
              }
            ],
            "duration": null,
            "expire_time": "1970-01-01T08:00:00+08:00",
            "id": "uhost-u2byoz4i",
            "image_id": "uimage-ku3uri",
            "instance_type": "n-basic-2",
            "ip_set": [
              {
                "internet_type": "Private",
                "ip": "10.25.94.58"
              }
            ],
            "isolation_group": "",
            "memory": 4,
            "min_cpu_platform": null,
            "name": "tf-example-normal-instance",
            "private_ip": "10.25.94.58",
            "remark": "",
            "root_password": "supersecret1234",
            "security_group": "firewall-a0lqq3r3",
            "status": "Running",
            "subnet_id": "subnet-0czucaf2",
            "tag": "tf-example",
            "timeouts": null,
            "user_data": null,
            "vpc_id": "uvnet-0noi3kun"
          },
          "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=",
          "dependencies": [
            "data.ucloud_images.default",
            "data.ucloud_security_groups.default"
          ]
        }
      ]
    }

可以看到root_password的值supersecret1234是以明文形式被写在tfstate文件里的。这是Terraform从设计之初就确定的,并且在可见的未来不会有改善。不论你是在代码中明文硬编码,还是使用参数(variable,我们之后的章节会介绍),亦或是妙想天开地使用函数在运行时从外界读取,都无法改变这个结果。

解决之道有两种,一种是使用Vault或是AWS Secret Manager这样的动态机密管理工具生成临时有效的动态机密(比如有效期只有5分钟,即使被他人读取到,机密也早已失效);另一种就是我们下面将要介绍的——Terraform Backend

tfstate管理方案——Backend

到目前为止我们的tfstate文件是保存在当前工作目录下的本地文件,假设我们的计算机损坏了,导致文件丢失,那么tfstate文件所对应的资源都将无法管理,而产生资源泄漏。

另外如果我们是一个团队在使用Terraform管理一组资源,团队成员之间要如何共享这个状态文件?能不能把tfstate文件签入源代码管理工具进行保存?

把tfstate文件签入管代码管理工具是非常错误的,这就好比把数据库签入了源代码管理工具,如果两个人同时签出了同一份tfstate,并且对代码做了不同的修改,又同时apply了,这时想要把tfstate签入源码管理系统可能会遭遇到无法解决的冲突。

为了解决状态文件的存储和共享问题,Terraform引入了远程状态存储机制,也就是Backend。Backend是一种抽象的远程存储接口,如同Provider一样,Backend也支持多种不同的远程存储服务:

image.png

Terraform Remote Backend分为两种:

  • 标准:支持远程状态存储与状态锁
  • 增强:在标准的基础上支持远程操作(在远程服务器上执行plan、apply等操作)

目前增强型Backend只有Terraform Cloud云服务一种。

状态锁是指,当针对一个tfstate进行变更操作时,可以针对该状态文件添加一把全局锁,确保同一时间只能有一个变更被执行。不同的Backend对状态锁的支持不尽相同,实现状态锁的机制也不尽相同,例如consul backend就通过一个.lock节点来充当锁,一个.lockinfo节点来描述锁对应的会话信息,tfstate文件被保存在backend定义的路径节点内;s3 backend则需要用户传入一个Dynamodb表来存放锁信息,而tfstate文件被存储在s3存储桶里。名为etcd的backend对应的是etcd v2,它不支持状态锁;etcdv3则提供了对状态锁的支持,等等等等。读者可以根据实际情况,挑选自己合适的Backend。接下来我将以consul为范例为读者演示Backend机制。

1.Consul简介以及安装

Consul是HashiCorp推出的一个开源工具,主要用来解决服务发现、配置中心以及Service Mesh等问题;Consul本身也提供了类似ZooKeeper、Etcd这样的分布式键值存储服务,具有基于Gossip协议的最终一致性,所以可以被用来充当Terraform Backend存储。

安装Consul十分简单,如果你是Ubuntu用户:

curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install -y consul

对于CentOS用户:

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install consul

对于Macos用户:

brew tap hashicorp/tap
brew install hashicorp/tap/consul

对于Windows用户,如果按照前文安装Terraform教程已经配置了Chocolatey的话:

choco install consul

安装完成后的验证:

$ consul

安装完Consul后,我们可以启动一个测试版Consul服务:

$ consul agent -dev

Consul会在本机8500端口开放Http终结点,我们可以通过浏览器访问http://localhost:8500

image.png

3.使用Backend

我们写一个可以免费执行的简单Terraform代码:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
  backend "consul" {
    address = "localhost:8500"
    scheme  = "http"
    path    = "my-ucloud-project"
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
}

注意要把代码中的public_key、private_key和project_id换成你自己的。

在terraform节中,我们添加了backend配置节,指定使用localhost:8500为地址(也就是我们刚才启动的测试版Consul服务),指定使用http协议访问该地址,指定tfstate文件存放在Consul键值存储服务的my-ucloud-project路径下。

当我们执行完terraform apply后,我们访问http://localhost:8500/ui/dc1/kv

image.png

可以看到my-ucloud-project,点击进入:

image.png

可以看到,原本保存在工作目录下的tfstate文件的内容,被保存在了Consul的名为my-ucloud-project的键下。

让我们执行terraform destroy后,重新访问http://localhost:8500/ui/dc1/kv

image.png

可以看到,my-ucloud-project这个键仍然存在。让我们点击进去:

image.png

可以看到,它的内容为空,代表基础设施已经被成功销毁。

4.观察锁文件

那么在这个过程里,锁究竟在哪里?我们如何能够体验到锁的存在?让我们对代码进行一点修改:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
  backend "consul" {
    address = "localhost:8500"
    scheme  = "http"
    path    = "my-ucloud-project"
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
  provisioner "local-exec" {
    command = "sleep 1000"
  }
}

这次的变化是我们在ucloud_vpc的定义上添加了一个local-exec类型的provisioner。provisioner我们在后续的章节中会专门叙述,在这里读者只需要理解,Terraform进程在成功创建了该VPC后,会在执行Terraform命令行的机器上执行一条命令:sleep 1000,这个时间足以将Terraform进程阻塞足够长的时间,以便让我们观察锁信息了。

让我们执行terraform apply,这一次apply将会被sleep阻塞,而不会成功完成:


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:

  # ucloud_vpc.vpc will be created
  + resource "ucloud_vpc" "vpc" {
      + cidr_blocks  = [
          + "10.0.0.0/16",
        ]
      + create_time  = (known after apply)
      + id           = (known after apply)
      + name         = (known after apply)
      + network_info = (known after apply)
      + remark       = (known after apply)
      + tag          = "Default"
      + update_time  = (known after apply)
    }

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

ucloud_vpc.vpc: Creating...
ucloud_vpc.vpc: Provisioning with 'local-exec'...
ucloud_vpc.vpc (local-exec): Executing: ["/bin/sh" "-c" "sleep 1000"]
ucloud_vpc.vpc: Still creating... [10s elapsed]
...

让我们重新访问http://localhost:8500/ui/dc1/kv

image.png

这一次情况发生了变化,我们看到除了my-ucloud-project这个键之外,还多了一个同名的文件夹。让我们点击进入文件夹:

image.png

在这里我们成功观测到了.lock和.lockinfo文件。让我们点击.lock看看:

image.png

Consul UI提醒我们,该键值对目前正被锁定,而它的内容是空。让我们查看.lockinfo的内容:

image.png

.lockinfo里记录了锁ID、我们执行的操作,以及其他的一些信息。

让我们另起一个新的命令行窗口,在同一个工作目录下尝试另一次执行terraform apply

$ terraform apply
Acquiring state lock. This may take a few moments...

Error: Error locking state: Error acquiring the state lock: Lock Info:
  ID:        563ef038-610e-85cf-ca89-9e3b4a830b67
  Path:      my-ucloud-project
  Operation: OperationTypeApply
  Who:       byers@ByersMacBook-Pro.local
  Version:   0.13.5
  Created:   2020-11-16 11:53:50.473561 +0000 UTC
  Info:      consul session: 9bd80a12-bc2f-1c5b-af0f-cdb07e5e69dc


Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.

可以看到,同时另一个人试图对同一个tfstate执行变更的尝试失败了,因为它无法顺利获取到锁。

让我们用ctrl-c终止原先被阻塞的terraform apply的执行,然后重新访问http://localhost:8500/ui/dc1/kv

image.png

可以看到,包含锁的文件夹消失了。Terraform命令行进程在接收到ctrl-c信号时,会首先把当前已知的状态信息写入Backend内,然后释放Backend上的锁,再结束进程。但是如果Terraform进程是被强行杀死,或是机器掉电,那么在Backend上就会遗留一个锁,导致后续的操作都无法执行,这时我们需要用terraform force-unlock命令强行删除锁,我们将在后续详细叙述。

点赞(0)

评论列表 共有 0 评论

暂无评论

微信服务号

微信客服

淘宝店铺

support@elephdev.com

发表
评论
Go
顶部