前言
Terraform 是 Hashicorp 公司开源的一种多云资源编排工具。使用者通过一种特定的配置语言(HCL, Hashicorp Configuration Language)来描述基础设施,由 Terraform 工具统一解析,构建资源之间的关系,生成执行计划,并通过调用各家云厂商的具体实现来完成整个基础设施生命周期的管理。
相对于其它的云上资源管理方式,Terraform 的主要特点有:
基于 IaC(基础设施即代码,Infrastructure as Code)的设计,可以将基础设施以一种领域特定语言描述出来,消除了在基础设施自动化时描述语义上的歧义,同时减轻了人为因素造成的不确影响。
Terraform 在执行编排动作前,会生成一份可读性良好的执行计划,关键基础设施的变更可以得到充分审查,保证了基础设施的可靠性。
基于 DAG(有向无环图,Directed Acyclic Graph)描述资源与资源之间的关系,由于 DAG 良好的拓扑性质,当资源属性与资源关系发生改变时,变更动作将被充分并行地执行。
在 UCloud,我们最终选择了 Terraform 来编写 UCloud 基础设施代码,并配合 UCloud CLI、Ansible 等工具,进一步拓展了 Terraform 的功能,实现基础设施可编程。
本文将详细阐述 Terraform 的整个生命周期,从 Provider 开发者的视角,介绍 Terraform 在安全、效率和状态一致性三个方面的内部机理与具体实现。
技术实现解析
生命周期
以首次执行 Terraform 创建 UCloud 云上资源为例,这一资源编排动作的生命周期如下图所示:
图表 1 Terraform 生命周期
图中立方体所示分别为:
Terraform 核心进程:负责资源定义文件,构建有向无环图,管理状态存储;
Provider 进程:即提供资源编排能力的进程,包括由云厂商实现的能力(比如 UCloud),和应用程序提供的能力(比如 TLS)等;
Provisioner 进程:即提供资源编排后处理操作的进程,比如执行 Shell 命令,上传文件等;
以中央的有向无环图为分界线,左侧的部分是 Terraform 本身提供的能力,右侧是由云厂商提供的能力。
当执行 Terraform 命令首次编排云上资源时:
Terraform 首先唤醒核心进程,初始化 Backend(即状态管理组件);
解析用户编写的资源定义文件,同步最新的资源状态,并与当前的资源定义作对比;
初次构建 DAG 时,资源尚未被初始化,所以资源状态为空,用户的资源实例都将作为 DAG 中新增的节点被创建。
在并行构建资源时:
并行遍历 DAG;
当遇到 Provider 节点时,Terraform 核心进程唤醒 UCloud Provider 进程;
将所有的编排动作依次发给 UCloud Provider;
Provider 调用 UCloud OpenAPI 管理云上资源;
返回的结果由 Terraform 核心进程写回状态存储。
进程管理
随着云计算的普及,以及人们对于数据安全性和可用性上的考量,越来越多的企业开始意识到,不能把鸡蛋放在一个篮子里,基础设施的中立和非绑定是云服务商十分关键的属性。
Terraform 中每一个云厂商的实现(Provider)都是一个独立的进程,进程间使用 RPC 通信的方式下发指令和交换数据,这样设计有什么好处呢?
安全性:多云环境下,进程隔离云厂商的实现,防止共享内存带来的安全性问题。
扩展性:插件式的设计使得特性的增加更加容易,而官方插件仓库使得特性的质量更有保障。
稳定性:核心与插件分离,保证了核心简单可靠,测试充分。单一插件的 Bug 不会扩散到全局。
而从使用者的角度来看,Terraform 多进程模型的重中之重,是多云环境下厂商隔离带来的安全性问题。安全性是多云编排的基石,如果无法保证云厂商之间的隔离性和安全性,多云编排则无从谈起。
Terraform 使用插件(Plugin = Provider + Provisioner)来抽象出各个云厂商之间的差异,并相互隔离。
图表 2 Terraform 多云插件管理
在一次编排任务的生命周期中,Provider 将会基于 Terraform 提供的能力,完成静态检查(Validate)、资源状态同步(Read/Refresh)、生成执行计划(Plan)、执行编排(Apply)等操作。
依赖管理
软件工程的实践表明,高层次的抽象,可以简化问题,让复杂的问题变得可以测试。而对于依赖关系的抽象,业界最通行的做法即使用有向无环图(DAG,Directed Acyclic Graph)来描述事务间的依赖关系。有向无环图上的点即事物本身,边则是事物与事物之间的联系。
业界对于 DAG 的使用极为广泛,比较典型的是各种大数据工作流引擎,比如 Oozie,Airflow 等。在这些引擎中,批处理任务作为 DAG 上的节点,而任务间的依赖作为 DAG 上的边。
图表 3 业界常见的 DAG 应用
Terraform 将所有的资源构建为一张有向无环图(DAG),计算它们的依赖关系,并行地去创建和修改相互间没有依赖的那些资源。因此整个基础设施的构建过程是高效且严格有序的。
下面我们将举例介绍它的内部原理和实现。
注意:文中的图与描述为了呈现效果,分别有所简略,仅供参考
图构建
假设一个场景,一台主机与一个公网弹性 IP 绑定,且客户使用自有的第三方 DNS 服务,通过 A 记录指向该主机的公网 IP。
图表 4 构建云上资源拓扑
这个场景展现了 Terraform 对于资源拓扑关系的描述能力,以及对于外部服务的集成能力。它背后的工作原理是什么样的呢?
所有云上资源,都抽象为 DAG 的一个节点,而资源与资源之间的关系,则有两种抽象方式:
一种是抽象为边,将两个资源节点连接在一起,例如从 dnssimple_record 到 ucloud_eip 的箭头表示将 DNS 指向弹性 IP(eip);
另一种是抽象为一个单独的资源,例如 eip_association 资源将弹性 IP(eip)和云主机(instance)绑定在一起。
图变换
图表 5 有向无环图变换
在图构建的过程中,Terraform 需要对 DAG 进行若干次变换(Transform)操作,如:
添加辅助节点,如 Config、Variable、Local、Provider Node、Root Node 等;
附加辅助信息到 Resource Node;
添加清理操作节点,作为图遍历过程中末端的节点,例如 CloseProvider/Provisioner;
进行图化简操作,例如做 transitive reduction 简化多余的边,减少编排成本。
图遍历
最后对整张图进行遍历,对每一个资源节点分别执行资源编排操作,比如读取、创建、更新和删除等。
图表 6 并行遍历,执行资源动作
从根节点开始,Terraform 并行地去编排整个资源拓扑,遍历整个有向无环图,直到所有资源都被成功编排,并执行清理操作。
可以看出,由于有向无环图出色的拓扑性质,整个遍历过程,存在着充分的局部并行化,编排时间跟基础设施复杂度有显著关系,而同构基础设施的规模则对编排时间影响较小,保证了 Terraform 在大规模水平扩展时拥有较好的性能。
状态管理
Terraform 引入了面向资源的设计,将资源的状态描述为一个状态的集合,并支持若干种不同类型的状态存储。
默认情况下,在 Terraform 的执行目录下,会存储一个本地的资源状态文件,并在每次编排开始时,从远程同步状态到本地,比较该状态与用户定义的资源之间的差异,从而生成编排计划。
定义
在这一抽象中,Terraform 官方给出了几个基本的定义:
从上文中的定义可以看出,执行计划(Plan)本质上就是 Diff 格式化输出的结果,而执行编排就是应用这个 Diff 的过程。
Backend
Terraform 将对资源状态的管理抽象出了一个统一的状态管理层(Backend),使得基于 Terraform 的资源编排系统可以保持基础设施的一致性。
想象一个场景,如果 A 同学在操作基础设施的变更,B 同学此时也想执行变更,这个变更会执行么?
图表 7 同时操作云上资源
答案显然是不会的,任何一个成熟的系统都应该对这样的问题提出解决方案。
Terraform Backend 通过对状态加锁来解决资源的竞态问题。A 在操作资源的时候状态会被锁定,此时 B 执行的任何变更行为都将被拒绝。
其中,consul、etcd 和 http 是比较推荐的扩展:
consul、etcd 提供了锁机制,且基于 Raft 协议保证了数据的强一致性;
http 适用于自行研发的,可扩展的状态存储,如云服务商提供的状态托管服务,可选支持锁机制。
Terraform 对 Backend 的抽象增强了状态存储的可扩展性,同时提供了可选的锁机制扩展,基于此云厂商可以定义自己的远程状态存储,用于托管用户的资源状态,并为用户提供可靠的并发安全保障。
时效性
Terraform 的杀手级特性之一 —— 执行计划,允许导出执行计划,延后执行,提供了在 CI/CD 环境下人工审查执行计划的可能,对关键基础设施变更的安全性提供了保障。所以,导出的执行计划是否过期是生产环境中最常见的问题。
Terraform 如何保证已经失效的执行计划不再被执行?Terraform 使用多版本快照(Multi-Version Snapshot)的方式来实现。可以类比于常规的 MVCC(多版本并发控制)来理解,下图是一个最小化的 MVCC 实现:
图表 8 常规的多版本 MVCC 实现
进程 P1 和 P2 依次读到了序号为 1 的数据,并且都想进行写操作,P1 先修改数据,自增序号为 2 并写入成功,此时 P2 进行写操作时,由于修改后的序号同样为 2,此时应抛出写失败,P2 需要主动重新读取最新的数据再次修改,才能成功写入。
由于 Terraform 可以执行一个已导出的执行计划,一个事务的时间被极大延长了,所以版本冲突的可能被无限放大。
基于此,Terraform 同样选择该方式,通过一个序号来标识状态的版本,当执行计划的状态序号小于当前状态的序号时,直接丢弃过时的执行计划:
图表 9 Terraform 的执行计划过期实现
基于这样的原理,Terraform 保证了导出的执行计划是有时效性的。例如一个用户导出了一份执行计划,将云主机从 1 台水平扩容到 3 台,但在该执行计划审查通过之前,另一个用户已经扩容到 5 台云主机,此时这份执行计划执行时会 Abort,而不会从 5 台降为 3 台,从而保证关键基础设施的变更是安全的。
状态升级
在软件构建或产品设计中,不可避免的会出现一些破坏性的变更,而这些破坏性的变更又不可避免地会影响资源状态。
图表 10 状态迁移的痛点
常见的在线服务(比如 HTTP API)设计中,通常在 HTTP Header 甚至 URL 加一个版本号来区分新老版本。但 Terraform 作为一个二进制分发的软件,且在用户的本地存储有一份资源状态,如果进行破坏性的升级,则必须同时考虑存量用户的正常使用,以及旧版状态文件的原地升级。
Terraform 的解决方案是标定资源的 Schema Version,默认 Version 为 0,当资源的 Schema 有破坏性的更改时,作为 Provider 的云厂商必须为此提供一个原地升级函数。
图 11 资源状态平滑升级
假设 UCloud 的 EIP 共有 3 个版本,0,1,2,则须提供 2 个升级函数。
当一个使用版本 0 的用户升级到最近版本 2 执行编排的时候,他的状态文件将会依次经过两个升级函数,将资源状态原地升级至最新版本的状态,而无需任何额外操作。
总结
总体而言,Terraform 是一个安全的,可扩展的,有扎实的理论基础,也有渐进式工程实践的资源编排工具。Terraform 的关键特性:基础设施即代码、多云编排、执行计划与过程分离、统一的资源状态管理,是我们在新一代资源编排系统实践中的重要保障。
作者简介
李宇飞,UCloud 后台研发工程师,参与资源编排等接入产品项目研发,专注于云计算,DevOps,分布式系统等领域。
评论