# 介绍一个小工具：Terranetes

## IaC 不只是 Terraform

虽然几年前的一次讨论中，我嘲讽过某同事说，Terraform 目前最靠谱的 Provider，也就只有 Kubernetes 一个而已，相对于顾头不顾尾的 Provider 来说，Kubernetes + Operator 才是正道；然而形势比人强，目前 Terraform 的确是能帮用户踏上 IaC 旅程的方便法门。

就像 Kubernetes 之于云原生，对于 IaC 来说，Terraform 也同样有着 Day2 的问题。并且由于面对更大范围、更大成本的调度能力，Terraform 的管控也面临更大的挑战。例如工作流、合规、安全、成本等方面的考虑，以及偏差检测、模块安全等特性的应用，都是摆在管理员面前的明显问题。围绕这一工具，想要构建一个稳健的自助平台，是个颇为复杂的困难过程。

## Terranetes 简介

`appvia.io` 是一个位于伦敦的解决方案厂商，它的开源项目 `Terranetes` 就尝试解决 Terraform 的 Day2 问题。这个 golang 项目在 22 年开源，到现在为止，发布了一百多个 Release，Star 数量只有 135 个，上次代码更新是在两个月前，从运作成绩来看，似乎不太成功。然而从平台工程的角度来看，这个项目提供的众多特性，却是非常值得关注和致敬的。

Terranetes 首页有这样一张图，形象地描述了这一产品的 IaC 治理思路：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745946442559/36f3c894-3eee-42d4-b2ce-4ea5aa8b4a45.png align="center")

### 工作流

图里简单地描述了一个工作流，平台团队制定规则、开发团队申请资源，控制器接收申请、引用凭据、执行规范，按照流程中设定的审批、Lint 等环节，完成资源申请过程。

他把用户明确地区分为平台和开发两种角色：

#### 平台团队

平台用户的主要职责是为开发者用户提供自助服务的基础支持：

* **接入资源供应商**：用 CRD 的形式定义资源供应商，以此来支撑开发者使用指定供应商的资源。
    
* **凭据管理**：管理接入资源供应商的身份凭据，开发者无需自行管理凭据，即可获得操作资源的权限。
    
* **策略管理**：从成本、合规、安全等方面，提供策略支持，保障开发者对资源的有效使用。
    

#### 开发团队

开发者的职责相对集中一些：引用平台团队维护的凭据、规则、模板等，按照既定工作流进行资源的申请和使用。

## 上手一试

官方提供了 Quick Start 文档，网络条件允许的话，几分钟就可以完成第一次资源分配。下面的例子我使用 AWS 作为资源供应商，本机的 OrbStack 提供给 Terranetes 作为控制器的 Kubernetes 运行平台。

### 先决条件

* Kubernetes 集群
    
* AWS 账号，有能够创建 S3 Bucket 的 AK/SK
    

### 部署

基于 Helm 的老套部署方式：

```bash
$ helm repo add appvia https://terranetes-controller.appvia.io
$ helm repo update
$ helm install -n terraform-system terranetes-controller appvia/terranetes-controller --create-namespace
...
```

运行后可以看到多出了 `terraform-system` 命名空间，其中运行了两个控制器 Pod。

### CRD

看看多出了哪些 CRD：

```bash
$ kubectl api-resources | grep terraform
cloudresources                                 terraform.appvia.io/v1alpha1      true         CloudResource
configurations                                 terraform.appvia.io/v1alpha1      true         Configuration
contexts                                       terraform.appvia.io/v1alpha1      false        Context
plans                                          terraform.appvia.io/v1alpha1      false        Plan
policies                                       terraform.appvia.io/v1alpha1      false        Policy
providers                                      terraform.appvia.io/v1alpha1      false        Provider
revisions                                      terraform.appvia.io/v1alpha1      false        Revision
```

官网文档用下图来描述对象之间的关系：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745946456953/9311a01f-6748-4a85-9f51-2293e3b5d000.png align="center")

**CloudResources**：这个对象用来描述 Terraform 中的云资源，CloudResources 会选择性的向用户公开属性，从而减少开发者的心智负担，并确保选项符合组织意图。

**Revisions**：Revision 是云资源的模板，在实际环境中，应该是经过策划和测试的版本化资产，其中会包含组织所需的默认设置，并只公开与上下文相关的功能：

* Revision 指向 Terraform 模块。
    
* 包含平台希望模块默认设置的所有默认选项，并向消费者公开部分可见选项。
    
* 可以跟踪 Terraform 模块，也可独立演进。
    
* 所有 CloudResources 都会引用集群中的 Revision。
    
* 要升级 CloudResource 消费者，需要更新其指向的 Revision。
    

**Configurations**：另一种描述云资源的方法，相对于 CloudResource，它相对传统一些，采用了和 Module 进行一对一连接的方式。

**Providers**：用来连接云资源提供方的定义。

**Plans**：无需主动创建，随 `Revision` 对象的定义自然产生。

**Policies**：对策略的引用，其中包含了 `Checkov` 规则的引用方法、对 Module 的限制。

**Contexts**：上下文提供了一种在配置之间共享通用配置的方法。集群中任何组件都可以引用该资源。

### 配置凭据和 Provider

接下来配置用来连接 AWS：

```bash
$ kubectl -n terraform-system create secret generic aws \
--from-literal=AWS_ACCESS_KEY_ID=${AWS_AK} \
--from-literal=AWS_SECRET_ACCESS_KEY=${AWS_SK} \
--from-literal=AWS_REGION=${AWS_AGN}
secret/aws created
$ export PROVIDER="https://raw.githubusercontent.com/appvia/terranetes-controller/master/examples/provider.yaml"
$ kubectl apply -f $PROVIDER
provider.terraform.appvia.io/aws created
provider.terraform.appvia.io/aws-irsa created
```

可以看到 Provider 对象的 `spec` 内容：

```yaml
  spec:
    preload:
      cluster: wayfinder-production
      context: default
      enabled: false
      region: eu-west-2
    provider: aws
    secretRef:
      name: aws
      namespace: terraform-system
    source: secret
```

其中指定了 AWS 作为资源供应商，并且使用前面的 Secret 作为连接凭据。

### 配置 Revision

```bash
$ export REV="https://raw.githubusercontent.com/appvia/terranetes-controller/master/examples/revision.yaml"
$ kubectl apply -f ${REV}
revision.terraform.appvia.io/bucket.v1 created
```

打开 Revision 文件，能够大致观察到其中包含的信息：

* **configuration**：其中有 Module 的地址、Context 引用、Provider 引用以及**默认值**。
    
* **inputs**：这里定义了用户可以控制的内容（此处只有 `bucket` 可写）。
    
* **plan**：则定义了该对象所属的计划。
    

`Revision` 中定义的 `Plan` 也被自动创建了：

```bash
$ kubectl get plan bucket -o yaml
apiVersion: terraform.appvia.io/v1alpha1
kind: Plan
metadata:
  ...
spec:
  revisions:
  - name: bucket.v1
    revision: v0.0.1
```

## 使用 Plan-&gt;Revision 创建资源

上面两节完成了平台管理或者资源管理角色的任务，接下来要真正地申请资源了。这里需要创建如下的 `CloudService` 对象：

```yaml
apiVersion: terraform.appvia.io/v1alpha1
kind: CloudResource
metadata:
  name: bucket
spec:
  plan:
    name: bucket
    revision: v0.0.1
  providerRef:
    name: aws
  writeConnectionSecretToRef:
    name: test
  variables:
    bucket: forever8384
```

执行之后，我们会看到，系统中有了一个运行成功的 `Job` 对象，以及一个 `CloudResource` 对象：

```bash
$ kubectl get jobs
NAME                  COMPLETIONS   DURATION   AGE
bucket-tgbrl-1-plan   1/1           28s        8m37s
$ kubectl get cloudresources.terraform.appvia.io
NAME     PLAN     REVISION   SECRET   CONFIGURATION   ESTIMATED     UPDATE   SYNCHRONIZED   AGE
bucket   bucket   v0.0.1     test     bucket-tgbrl    Not Enabled   None     OutOfSync      8m50s
```

任务已经完成，打开 AWS 控制台，也找不到我们要的 Bucket。看看 Job 对应的 Pod 日志：

```bash
kubectl logs -f bucket-tgbrl-1-plan-lj7hd
[info] Checking if required flags have been provided.
[info] Waiting 10 seconds for pod logs to be available (attempt 1/15)..
[info] waiting for the job to be scheduled
...
  # aws_s3_bucket_server_side_encryption_configuration.this[0] will be created
  + resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
...
  # aws_s3_bucket_versioning.this[0] will be created
  + resource "aws_s3_bucket_versioning" "this" {
...
Plan: 6 to add, 0 to change, 0 to destroy.
```

搞 Terraform 的读者会看得出来，他似乎只做了 Plan，没有做实际的 Apply。

文档解释说，默认情况下，资源的创建是需要被批准的，除非是使用了注解：`terraform.appvia.io/apply=true`，所以我们现在加入这个：

```bash
$ kubectl annotate cloudresources bucket "terraform.appvia.io/apply"=true --overwrite
cloudresource.terraform.appvia.io/bucket annotated
```

接下来会看到一个新的 Job 在运行，类似 `bucket-l6jkf-1-apply-xp45d` 这样的名称。这个 Pod 运行成功之后，会看到 S3 桶已经创建。

### 客户端

Terranetes 还有个叫 `tnctl` 的命令行客户端软件，提供了任务跟踪、审批等能力，避免大量使用 `kubectl` 来完成这些任务。

## 其他

### 偏差检测

`CloudResource` 有个字段 `spec.enableDriftDetection`，设置为 `True` 就可以启动偏差检测。例如上面的 CloudResource，我们修改这个字段为 True 之后，删除对应的桶，一段时间之后，这个资源就会变成 `OutOfSync` 状态。

### 监控

Controller 自带了 Prometheus 的指标抓取端口，提供了请求数、延迟时间等简单的指标。

除了这些基本功能之外，Terranetes 还能对接 `Infracost` 进行成本数据采集和预测，不过这是个商业产品，就没有进一步尝试了。

总之，Terranetes 提供了一个相对全面的框架，其中展示的工作流、策略、分权等设计，都是很好的范本，很值得工具平台、IaC 相关方案的设计者们参考和学习。
