terraform-必知必会

本文记录一些关于terraform的理解,只记录我认为有必要记录的东西,以及一些我自身的理解,经验部分来自我自身面试经历,本文绝大部分来自教程: https://lonegunmanb.github.io/introduction-terraform/

概述

HashiCorp这家公司,极其重视”基础设施”建设,他们思考terraform的配置文件应该使用json或yaml时,都不满意,于是设计了HCL(HashiCorp Configuration Language).他们又花时间开发了 go-plugin 这个项目,把插件编译成一个独立进程,与主进程通过 rpc 进行互操作。

Terraform是一种基础设施即代码(infrastructure as code)的实现.允许开发人员使用声明式语言定义和管理基础设施资源.

原理

  1. 声明式语言: Terraform使用声明式语言语言来定义/描述你想要的额基础设施的最终状态,而不需要关心怎么达到该状态.
  2. 资源提供者(provider): Terraform通过provider与各种基础设施平台(AWS, GCP, K8S)交互,不同的provider负责不同平台的API通信已经资源的管理.provider也就是terraform的插件,与terraform主进程通过rpc交互.
  3. 状态管理: Terraform使用状态文件statefile来记录当前基础设施的状态.这个文件记录所有已创建的资源及其配置信息,以便在后续的执行中进行比较和更新.
  4. 执行计划: terraform plan 它会分析当前状态文件和代码,对比并生成一个执行计划,计划显示将要创建/更新/删除的资源以及顺序.
  5. 应用计划: terraform apply 应用上面的计划,实际改变平台资源.
  6. 可重现性: 无论何时运行terraform,它都会尽力使基础设施达到你所定义的状态,如果已经符合,则不会做出任何改变.

Provider

即terraform的go-plugin插件,不同平台对应不同的provider,terraform负责分析代码然后通过provider调用平台的api或者SDK实现资源的管理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
terraform {
required_providers {
ucloud = {
source = "ucloud/ucloud"
version = ">=1.24.1"
}
}
}

provider "ucloud" {
public_key = "your_public_key"
private_key = "your_private_key"
project_id = "your_project_id"
region = "cn-bj2"
}

provider的配置是全局的,只能配在根模块下.

terraform init重要一步就是下载provider到本地

状态文件

terrafrom的状态文件主要包含三个状态信息:

  1. 资源当前状态
  2. 配置中定义的期望状态
  3. 上面二者之间的映射

状态锁

State Lock是一种机制,用于在并行执行Terraform命令时出现资源抢占的问题.状态锁确保同一时间只有一个terraform可以修改状态文件.

当运行一个terraform命令时, 它会生成一个锁文件并获取它,知道执行完成才会释放该文件.执行过程中其他terraform命令因为获取不到这个锁文件所以无法执行.

Backend

Backend用于存储和管理状态文件,包括读写锁定等操作.它可以是本地文件系统,也可以远程存储(GCS, S3等).

同时terraform支持http形式的backend,意味着我们可以简单写个http服务在实现backend,然后terraform就可以通过http请求管理.

另外terraform也支持主流 db来存储状态文件,意味着PostgreSQL db/MariaDB/SQL server 等数据库也能作为一个backend.

下面用python写个httpserver来实现简单的backend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask, request, jsonify

app = Flask(__name__)
state = {}

@app.route('/terraform/state', methods=['GET'])
def get_state():
return jsonify(state)

@app.route('/terraform/state', methods=['POST'])
def update_state():
new_state = request.json
state.update(new_state)
return jsonify({'message': 'State updated successfully'})

if __name__ == '__main__':
app.run()

1
2
3
4
5
terraform {
backend "http" {
address = "http://your-backend-address:port/terraform/state"
}
}

上面只是实现了GET/POST,也就是获取和更新状态文件的功能,实际生产中还要实现文件的持久化、并发访问控制、状态锁定等功能.

terraform常见文件

  • main.tf: 类似函数中的main()
  • ouput.tf: 定义terraform的输出信息,在terraform执行完成后输出
  • data.tf: 定义数据源,用于查询平台上资源的信息.
  • variables.tf: 定义terraform的变量
  • terraform.tfvars: 定义terraform的变量默认值,它可以包含变量的默认值或敏感信息
  • provider.tf: 定义具体的provider,以及它的版本.
  • terraform.tfstate: 状态文件,一般存在远端backend
  • versions.tf: 定义terraform的版本要求

代码编写

terrafrom使用的HCL声明式语言,以块的形式组织代码,通过大括号来划分块,常用的块有:

  • local{}: 定义本地变量
  • resource{}: 定义资源
  • data{}: 用于数据查询或计算
  • module{}: 定义/引用模块
    • source定义模块位置
  • ouput{}: 输出数据

环境变量

  • TF_LOG: 定义terraform内部日志输出级别

    1
    export TF_LOG=[TRACE|DEBUG|INFO|WARN|ERROR]
  • TF_LOG_PATH: 顾名思义,定义日志输出文件

    1
    export TF_LOG_PATH=./terraform.log
  • TF_INPUT: 定义是否交互式执行terraform命令, 0或者false代表不交互

    1
    export TG_INPUT=0
  • TF_VAR_name: 此变量用于给name变量赋值

    1
    2
    3
    4
    export TF_VAR_region=us-west-1
    export TF_VAR_ami=ami-049d8641
    export TF_VAR_alist='[1,2,3]'
    export TF_VAR_amap='{ foo = "bar", baz = "qux" }'
  • TF_CLI_ARGS: 定义使用命令行时,默认附加给CLI的参数,比如TF_CLI_ARGS="-input=false" terraform apply -force相当于执行terraform apply -input=false -force,用于自动化CI环境下定制terraform的默认参数.

  • TF_CLI_ARGS_name: 则可以指定特定的子命令下定制默认参数.比如TF_CLI_ARGS_plan="-refresh=false"就只针对plan命令起作用.

  • TF_DATE_DIR: 定义工作目录下terraform数据的保存位置,默认是.terraform.一般情况下不应该改动它

  • TF_IN_AUTOMATION: 如果该值定义为非空值,terraform会认为自己运行在一个自动化环境下,从而减少一些非必要的输出信息,比如给出该执行什么子命令的建议.

  • TF_REGISTRY_DISCOVERY_RETRY和TF_REGISTRY_DISCOVERY_TIMEOUT: 定义连接registry下载provider或plugin的重试次数和超时时间(默认10s)

  • TF_CLI_CONFIG_FILE: 定义terraform命令行配置文件的位置

    1
    export TF_CLI_CONFIG_FILE="$HOME/.terraformrc-custom"

常用命令

  • init: 初始化terraform工作目录,包括下载provider,反复执行是安全的

  • plan: 创建变更计划, -out将输出保存在一个文件中

  • apply: 执行计划

  • destroy: 销毁资源

  • import: 将资源导入到状态文件,一般用于将非terraform管理的资源纳入terraform管理中,也可以将资源的状态文件导出到本地

  • validate: 检查目录下的terraform代码语法,只是检查语法不会访问远端资源

  • state: 对状态文件进行管理操作,一般情况下不建议手动更改state文件,此时可以使用state命令来对资源进行细粒度处理, 它有很多子命令, 例如:

    • terraform state list: 列出当前配置中所有资源及其状态, 它会显示当前状态文件中存在的所有资源的地址.
    • terraform state show: 显示指定资源的详细信息, 包括其属性和当前状态.
    • terraform state rm: 从状态文件中删除指定资源,但不删除实际资源只是剔除出terraform的管理
    • terraform state pull: 从远端backend拉取最新的状态文件到本地
    • terraform state push: 将本地状态文件推到远端backend
    • terraform state mv: 代码移动或重命名后资源文件中也要做对应的操作
    • terraform state refresh:刷新状态文件以获取最新的资源状态。它用于将当前的资源状态与实际的基础设施进行同步
    • terraform state lock/unlock: 手动锁定/解锁状态文件
  • output: 用来提取输出

  • fmt: 格式化terraform代码文件的格式和规范

  • graph: 用来生成代码描述的技术设施或执行计划的可视化视图,输出DOT格式,可以通过GraphViz转换成图形文件

    • terraform graph | dot -Tsvg > graph.svg
      
      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

      # 经验

      ## 状态文件迁移

      假设在本地init并apply,会在本地产生statefile,此时添加新的backend配置再次init.

      此时terraform会检测到backend改变了,它会先检查新backend有没有状态文件,没有的话就会直接迁移,需要用户手动确认.

      如果新backend已经有状态文件了,terraform会把`新旧`状态文件下载到本地的一个临时目录,然后要求用户人工核对后决定是否覆盖既有的statefile.

      ## backend配置错误

      第一次init时会报错连不上backend,修改好后再次init还是会报错.

      这是因为terraform在第一次init连不上backend时会在本地写statefile,第二次连接发现backend配置变了,它会尝试从原来的backend读取statefile,并且尝试将其迁移到新的backend,但是因为原来的backend本身就是错的,所以它还是会报错.

      `解决办法`是要把本地的statefile删掉或者移走.

      > 上面两个问题的原理其实都是一样的,那就是terraform在使用新backend的时候,都会做一个动作: `先检查旧的再判断新的`

      ## 代码结构改变

      当terraform配置的路径和结构与statefile不一致时,会丢失资源的跟踪.比如你把module.a移动到了module.b,apply时terraform会更根据现有的statefile去寻找资源,这些资源仍然关联着旧的路径(module.a),此时terraform会认为无法找到module.b这个资源,而尝试重新创建它. 因此路径变更如果未及时反映到状态文件中,会导致terraform状态与实际云环境的状态不一致,plan和apply可能会提出错误的改动计划,可能会导致资源无意中的破坏或重复创建.另外状态文件不更新也会导致依赖关系混论.

      ```shell
      # 更新状态文件的方法主要靠terraform state子命令
      # address: module.<模块名>.<资源类型>.<资源名称>
      # module路径改动
      terraform state mv module.old_module.resource_name module.new_module.resource_name
      # 在terraform管理中移除资源但不实际销毁它
      terraform state rm <address>
      # 列出当前terraform状态文件中所有资源
      terraform state list
      terraform state show <address>

terraform-必知必会
http://example.com/2024/01/31/terraform-must-know/
作者
Peter Pan
发布于
2024年1月31日
许可协议