# IaC 杂感

## IaC 的起源

IaC 是配置管理领域的一种技术，全称 Infrastructure as Code，字面意义：基础设施即代码，是一种使用可读文本发放和管理基础设施资源的方法。通常情况下，软件定义的基础设施管理平台，会为用户提供命令行、WebUI 的方式，让用户能够用手工或者工具化的方式进行资源发放和管理工作。随着“基础设施”这一概念的不断扩展，使用频度的不断提高，越来越多的基础设施平台会提供各自的 API 为自动化打开方便之门。为了更快、更多的发放更多种类的基础设施，用一致的代码对这些基础设施进行管控也是顺理成章。IaC 的发展史很清楚地证明了这种演进过程。

1993 年，Mark Burgess 在博士后期间，为了管理不同的 Unix 工作站，开发了 CFEngine。这个大概是 IaC 工具的鼻祖。据说他仅仅根据直觉和实践经验，为了简化在不同 Unix 下大量编写脚本的工作，而开发了这个软件，在这里他提出了面向最终状态进行收敛的思路。

2006 年，AWS 发布了 EC2，各种公有云、私有云随之兴起，企业面对的资源不再是少数的主机，取而代之的是数量更大、品种更多、生命周期更短的虚拟机和随之而来的、更复杂的 IT 环境。在这之后，Chef、Salt Stack、Ansible 等生态也先后浮出水面。

个人认为真正的变化，是接下来的 2021-2024 年，Cloudformation、Terraform 和 Kubernetes 陆续发布，使用声明式 API 进行 IaC 操作成了业界惯例。AWS Control API 和 Kubernetes 这样的基础设施，从底层保障了声明式 API 的实现能力。

## IaC 是对物理资源的采样

IaC 真的能描述物理资源么？很显然答案是否定的，毕竟现实世界是连续的。例如下面的 EC2 Instance：

~~~yaml
  MyEC2Instance: 
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: "ami-79fd7eee"
      KeyName: "testkey"
      BlockDeviceMappings: 
      - DeviceName: "/dev/sdm"
        Ebs: 
          VolumeType: "io1"
          Iops: "200"
          DeleteOnTermination: "false"
          VolumeSize: "20"
      - DeviceName: "/dev/sdk"
        NoDevice: {}
~~~

很明显，这几行代码不可能描述一个完整的 EC2 实例，结合前面提到的 Control API 和 Kubernetes，实际上，对资源的抽象从资源 API 层面就已经开始了，表现在 IaC 层面的，也只是这种抽象的结果。正如对声音的采样，IaC 中表达的资源不会是“完整的全貌”，越频繁的采样，能够保留越多的细节，也会造成这一描述的复杂度大大提高——但是无论如何提高，IaC 的描述能力甚至都达不到监控的细节水平。换句话说，使用 IaC 的方式来描述资源，就必须承担抽象带来的损失。

除了静态的属性之外，对象的状态也是对实际情况的大幅度抽象，例如下面的 Pod 状态：

~~~mermaid
stateDiagram-v2
    [*] --> Pending
    Pending --> Running : PodScheduled
    Pending --> Failed : PodFailed
    Pending --> Succeeded : PodSucceeded
    Running --> Succeeded : ContainersCompleted
    Running --> Failed : ContainersFailed
    Running --> Terminating : PodTerminating
    Terminating --> Succeeded : ContainersCompleted
    Terminating --> Failed : ContainersFailed
    Terminating --> [*]
    Failed --> [*]
    Succeeded --> [*]
~~~
虽然看起来很细致，但实际上中间忽略了很多细节，一个明显的例子就是，在各个状态之间切换失败时，往往都需要进一步的识别问题根因才能解决。

在《Thinking in Promises》中，有这样一段话：

> 我们的文化，偏好于对整体图景进行理解，这种偏好催生了控制系统：这些大型、集中式、无所不知的系统，像大脑一样运作运作，它们根据可用性和一致性的假设做出精确决策，根据我们的微观管理思路，产生直接的指令性的动作。集中化在逻辑上是合理的，然而它导致了规模上的限制。具备庞大处理能力的集中控制在逻辑上是合理的，然而在大规模系统下，仍然可能无法快速决策和执行动作。行动的延迟所导致的不准确和不一致，通常会造成未知后果。

随着计算、存储、网络的飞速进步，我们能够越来越多地获得系统中各种资源的细节信息，这可能会造成一种“膨胀”的心态——我们希望能够更多地获取系统中发生的所有细节，知晓其所有过往，甚至预测其所有未来。但是问题也很明显，我们面对的世界的复杂度的加速度，远高于我们的采集和管理能力的提升。将连续的物理资源抽象为离散的资源对象，并且以可读代码的方式进行表达，能有效地降低基础设施对注意力的消耗。同时针对软件开发过程设计的版本管理、访问控制、单元测试、文本比对、规则引擎、安全扫描、代码评审等一系列的方法都可以在 IaC 世界中大展拳脚，借助这一技术，管理员能更透明、更快、更大范围地对基础设施进行发放和管理。

## IaC、面向对象和微服务

在我使用 IaC 的这一段时间里，新鲜感过后，我遇到了和推广容器化同样的困境——大量的实际业务和管控需求无法满足。在云原生语境中，我通常会用 12 要素等微服务要求来解释为什么你的“微服务”不能容器化。例如其中对进程、状态、配置、快速启动和优雅终止等。但是在 IaC 的落地过程中，我感觉缺乏了这样的理论后盾，有些底气不足。手里有了锤子，自然希望一切都是钉子——这些基础设施不过就是微服务运行所依赖的环境吧？面向对象、微服务架构的各种原则在这里是否继续有效呢？Terraform Provider 开发最佳实践中有这么几条：

- Providers should focus on a single API or problem domain
- Resources should represent a single API object
- Resource and attribute schema should closely match the underlying API

很明显，这里将 TF Provider 视作了 Restful API 的延伸，而众所周知，Restful API 本身的设计，关注的也是“资源”及其 CURD-L 操作。因此作为 IaC 基础的 Provider 们，本身应该就可以用 OO 的方式进行构建了。IaC 资源就是对物理资源的抽象，我们在软件设计过程中所遵循的设计原则，应该也是适用于 IaC 的实施过程之中的，并且 IaC 应该是整个软件的一部分，因此其复杂度也是小于软件的整体的，顺着这个思路，就可以理直气壮的做些事了。
### 拆分堆栈

在公有云上运行软件，往往会涉及品类繁多的云资源，每次更新都是按照堆栈进行组织的，然而到底围绕一个微服务的堆栈应该由哪些资源组成呢？例如 VPC 算么？容器集群算么？浮动 IP 算么？按照上面的说法，把这些基础设施资源按照拆分微服务的方法进行组织，就方便多了。针对每个资源，简单地回答几个问题，就可以确定其归属了，例如记在谁的账上？谁在使用他？谁负责它的运维？哪些资源是一起更新的？这样几个维度判断下来，围绕着微服务，就能够构建合理的资源堆栈了。
### Module 的划分

通常会使用 Module 对资源进行组合，这种组合有很多好处，例如可复用、规范化、降低认知负载等等，然而什么资源和什么资源应该组合成一个 Module 呢？Module 类似于日常开发中的 Library，完全可以使用和共享代码一样的方式，确定其共享范围和功能边界。

### 流水线设计

在 AWS 的 Builder's Library 中提到：

> 典型的微服务可能具有应用程序代码管道、基础设施管道、操作系统修补管道、配置/功能标记管道，以及运算符工具管道。同一个微服务拥有多个管道有助于我们更快速地将更改部署到生产环境。未通过集成测试且阻塞应用程序管道的应用程序代码更改不会影响其他管道。例如，它们不会阻止基础设施代码更改到达基础设施管道的生产阶段。同一微服务的所有管道看起来都十分相似。例如，功能标记管道使用的安全部署技术与应用程序代码管道相同，因为错误的功能标记配置更改就像错误的应用程序代码更改一样，可能会影响生产。

不难发现，上述不同的流水线，也采用了类似微服务的划分方法，多条流水线以独立运作、互不堵塞的方式，用不同的频率个自运行。

## 入乡应随俗

如你所知，不管是面向对象，还是微服务架构，还没有、也不可能一统天下，IaC 也是这样。在传统运维领域，我们更倾向于掌控变更的全部过程，面对 IaC/Provider 这样的黑盒子，这种追求可能就有些不合时宜了——尤其是对于自行实现的 Provider 来说。

要想穿透 IaC 资源的状态管理，实现基于流程的过程管控，通常可以有两种做法：

1. 拆小堆栈：用尽可能小的颗粒度进行变更，这样就从宏观上提供了一个相对细致的管控能力。
2. 暴露细节：将原本被状态迁移隐藏起来的过程，暴露给外部进行观测和限制。

小颗粒的堆栈，牺牲的是 IaC 变更的效率和完整性；而暴露内部细节的方式，则破坏了对象的封闭性——像是暴露了所有 Private 成员的类定义，客观上对于“不规矩”/“不完整”的 Provider 是一种鼓励。

正如对 Kubernetes 对象的操作一样，IaC 的管控应该是一个相对“肤浅”、“粗放”的过程，能够依赖的，只能是 Provider 主动开放出来的状态迁移过程。
