在一个已经稳定运行的系统中,平台工程团队使用 Terraform 精准地管理着所有底层云资源——从 VPC 网络到 Kubernetes 集群,再到独立的 Solr 云主机集群。一切看起来井然有序,基础设施的变更遵循着严格的 GitOps 流程。然而,混乱潜藏在基础设施与应用配置的边界。应用开发团队需要频繁地变更 Kong API 网关的路由规则、插件配置,以及为新业务创建 Solr Core 并更新 Schema。
当前的流程是,应用团队向平台团队提 PR,修改一个庞大臃肿的 Terraform 项目。这导致了几个核心痛点:
- 发布耦合:一个简单的 Kong 路由变更,却要触发整个基础设施的
terraform plan
,执行速度缓慢且风险不可控。 - 权限失控:为了自治,一些团队要求获得 Terraform 的 apply 权限,这在安全上是完全无法接受的。
- 表达力受限:HCL 语言对于描述复杂的应用逻辑(例如,根据 Solr Schema 的某个字段动态生成 Kong 的 request-transformer 插件配置)力不从心,代码变得丑陋且难以维护。
问题非常明确:如何在维持基础设施稳定性的前提下,赋予应用团队安全、高效、声明式地管理其应用层配置(Application-as-Code)的能力?这本质上是一场关于工具链选择和架构责任边界划分的权衡。
方案A:在 Terraform 的世界里缝缝补补
最直接的想法是继续深化使用 Terraform。利用其 Provider 生态,将 Kong 和 Solr 的 API 也纳入 Terraform 的管理范畴。社区已经有了成熟的 Kong Provider,而 Solr 也可以通过一些社区 Provider 或者 terraform-provider-http
来勉强实现。
这种方案的核心思路是实现技术栈的统一。
# main.tf - 一个统一管理所有资源的项目结构
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kong = {
source = "kong/kong"
version = "~> 1.20"
}
}
}
# 基础设施层 (平台团队维护)
resource "aws_instance" "solr_node_1" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.large"
# ... 其他配置
}
resource "aws_instance" "kong_node_1" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.medium"
# ... 其他配置
}
# 应用配置层 (应用团队需要频繁修改)
# =========================================================
# 1. 为后端的商品搜索服务在 Kong 中创建 Service
resource "kong_service" "product_search_service" {
name = "product-search-service"
protocol = "http"
host = aws_instance.solr_node_1.private_ip
port = 8983
}
# 2. 为该服务创建 Route
resource "kong_route" "product_search_route" {
name = "product-search-route"
protocols = ["http", "httpshttps"]
methods = ["GET", "POST"]
hosts = ["api.example.com"]
paths = ["/search/products"]
service_id = kong_service.product_search_service.id
strip_path = true
}
# 3. 为该 Route 添加一个速率限制插件
resource "kong_plugin" "rate_limiting_for_search" {
name = "rate-limiting"
service_id = kong_service.product_search_service.id
config_json = jsonencode({
second = 5,
hour = 10000,
policy = "local"
})
}
优势分析:
- 单一语言和工具:团队无需学习新的工具,所有变更都在熟悉的 HCL 和 Terraform 工作流中完成。
- 状态一致性:理论上,从虚拟机到 API 路由的所有状态都由同一个 Terraform state 文件管理,避免了状态漂移。
劣势与陷阱:
这里的坑远比优势来得深刻。
- 状态文件锁定与污染:这是最致命的问题。当应用A的开发人员想修改
kong_route.product_search_route
时,他需要锁定整个 state 文件。如果此时平台团队正在进行网络变更,就会产生冲突。更糟糕的是,应用层的频繁变更会不断地在 state 文件中产生新的版本,将其历史记录与关键的基础设施变更日志混杂在一起。 - 爆炸半径不可控:
terraform apply
的影响范围是整个项目。一个错误的 HCL 变量引用,可能导致应用团队意外地删除了生产环境的数据库实例。在真实项目中,这种风险是不可接受的。 - Provider 的局限性:Kong Provider 相当成熟,但对于 Solr 这种 API 复杂的应用,情况就不同了。创建一个 Solr Core 并不仅仅是调用一个
CREATE
API endpoint。它可能涉及上传配置文件、设置副本、重载集合等一系列有序操作。用 HCL 来描述这种过程化的逻辑是一种折磨,通常需要借助null_resource
和local-exec
来调用脚本,这完全违背了 Terraform 的声明式初衷。
纯 Terraform 方案,试图用一把锤子解决所有问题,最终只会导致一个脆弱、臃肿且权限混乱的单体式 IaC 项目。它混淆了不同生命周期的资源,是一个典型的反模式。
方案B:引入 Ansible 作为应用配置层
既然问题出在职责不清,那么很自然地会想到引入一个专门的配置管理工具。Ansible 以其 Agentless 和简单的 YAML 语法成为一个有力竞争者。
架构会演变成这样:
- Terraform:继续负责其最擅长的领域:创建和管理底层、变更不频繁的云资源(VMs, Security Groups, Load Balancers)。其输出(Outputs)是 Ansible 需要的连接信息,如 Solr 和 Kong 的 IP 地址。
- Ansible:接管应用配置。通过独立的 Git 仓库和 CI/CD 流水线,运行 Playbook 来调用 Kong 和 Solr 的 Admin API。
# playbook-manage-app-config.yml
- name: Configure Kong Services and Solr Cores
hosts: localhost
connection: local
gather_facts: no
vars:
kong_admin_api: "{{ lookup('env', 'KONG_ADMIN_API_URL') }}" # 从CI/CD环境变量注入
solr_api_url: "{{ lookup('env', 'SOLR_API_URL') }}"
service_name: "product-search-v2"
solr_core_name: "products_v2"
tasks:
- name: Ensure Solr core '{{ solr_core_name }}' exists
uri:
url: "{{ solr_api_url }}/solr/admin/cores?action=CREATE&name={{ solr_core_name }}&instanceDir={{ solr_core_name }}&configSet=_default"
method: GET
status_code: 200
register: create_core_result
# 这里的幂等性检查很脆弱,真实场景需要检查core是否存在
changed_when: "'already exists' not in create_core_result.json.error.msg"
failed_when: create_core_result.status != 200 and 'already exists' not in create_core_result.json.error.msg
- name: Create or Update Kong Service for Solr
uri:
url: "{{ kong_admin_api }}/services/{{ service_name }}"
method: PUT
body_format: json
body:
name: "{{ service_name }}"
url: "{{ solr_api_url }}/solr/{{ solr_core_name }}"
status_code: [200, 201]
register: kong_service
- name: Create or Update Kong Route for the service
uri:
url: "{{ kong_admin_api }}/services/{{ service_name }}/routes/main"
method: PUT
body_format: json
body:
name: "main"
paths:
- "/search/products-v2"
strip_path: true
status_code: 200
优势分析:
- 职责分离:这是最大的进步。平台团队和应用团队现在工作在不同的代码库和流水线中,互不干扰。发布周期被解耦。
- 过程控制能力:对于创建 Solr Core 这种多步骤任务,Ansible 的有序 Task 执行模型比 HCL 更直观、更强大。
- 生态丰富:Ansible 有大量的 community modules,即使没有现成的,使用
uri
或command
模块调用 API 和 CLI 也非常方便。
劣势与陷阱:
- 缺乏真正的状态管理:Ansible 本质上是一个配置工具,而非状态管理工具。它不知道资源的“当前状态”。为了实现幂等性,每个 task 都需要开发者手动编写
changed_when
和failed_when
逻辑,这非常容易出错。如果有人手动删除了 Kong 的一个路由,Ansible 在下次运行时不会感知到这个“漂移”,除非你编写了专门的检查任务。 - 删除操作的风险:声明式工具如 Terraform 有一个巨大的优势:当你从代码中移除一个资源定义时,
plan
会明确告诉你它将被删除。在 Ansible 中,实现“清理”操作需要编写一个完全独立的 Playbook 或 Role,用于删除不再需要的配置。这使得资源的全生命周期管理变得复杂和危险。 - 测试困难:测试一个调用远程 API 的 Ansible Playbook 是出了名的困难。你需要依赖 Molecule 这样的工具和复杂的 Mock 环境。相比之下,
terraform plan
提供了一个无需真实执行就能预览变更的安全网。
Ansible 方案解决了职责分离的问题,但牺牲了声明式 IaC 的核心优势——状态管理和变更预览。它更像是一组自动化脚本的集合,而不是一个可靠的、描述最终状态的系统。
最终选择:引入 Pulumi 作为应用声明式管理层
我们需要一种工具,它既能像 Terraform 一样进行状态管理和声明式定义,又能像 Ansible 一样提供更强的编程能力和逻辑表达,同时还能与现有的 Terraform 基础设施无缝集成。Pulumi 完美地契合了这个角色。
最终的架构决策如下:
- **Terraform (平台层)**:职责不变,管理核心基础设施,并通过
terraform output
暴露出必要的端点信息,例如 Kong Admin API URL 和 Solr Host。 - **Pulumi (应用层)**:由应用团队在独立的代码库中维护。使用 TypeScript (或 Python/Go) 编写代码,声明式地定义所有 Kong 和 Solr 的应用配置。Pulumi 会读取 Terraform 的输出作为输入,实现两个世界的连接。
graph TD subgraph Platform Team Git Repo A[Terraform Code: main.tf] --> B{Terraform State}; end subgraph Application Team Git Repo D[Pulumi Code: index.ts] --> E{Pulumi State}; end subgraph CI/CD Pipelines P1[Platform CI/CD] -- Runs --> A; A -- Updates --> CloudInfra; P2[Application CI/CD] -- Runs --> D; D -- Updates --> AppConfig; end subgraph Cloud Environment CloudInfra[VMs, VPC, K8s, etc.]; AppConfig[Kong Routes, Solr Cores]; end B -- terraform_remote_state --> D; A -- Creates/Manages --> CloudInfra; D -- Configures --> AppConfig; AppConfig -- Sits on top of --> CloudInfra;
核心实现
首先,平台团队的 Terraform 项目需要输出关键信息。
# platform-infra/outputs.tf
output "kong_admin_api_endpoint" {
description = "The internal endpoint for Kong's Admin API."
value = "http://${module.kong.private_ip}:8001"
}
output "solr_api_endpoint" {
description = "The internal endpoint for Solr's API."
value = "http://${module.solr.private_ip}:8983"
}
接下来是应用团队的 Pulumi 项目。这是整个方案的精髓。
1. 项目设置
// package.json
{
"name": "app-config-as-code",
"dependencies": {
"@pulumi/pulumi": "^3.0.0",
"@pulumi/kong": "^1.1.0", // Pulumi's official Kong provider
"axios": "^1.6.0" // For custom Solr API calls
}
}
2. 主程序 index.ts
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as kong from "@pulumi/kong";
import axios from "axios";
// =============================================================================
// 1. 连接基础设施层:安全地读取 Terraform 的输出
// =============================================================================
const config = new pulumi.Config();
const platformStackName = config.require("platformStackName"); // e.g., "my-org/platform-infra/prod"
const platformStackRef = new pulumi.StackReference(platformStackName);
const kongAdminApiUrl = platformStackRef.getOutput("kong_admin_api_endpoint");
const solrApiUrl = platformStackRef.getOutput("solr_api_endpoint");
// =============================================================================
// 2. 封装复杂性:为 Solr Core 创建一个动态 Provider
// 这是 Pulumi 强大之处,当没有官方 Provider 时,可以自己封装任何 API。
// =============================================================================
interface SolrCoreInputs {
name: pulumi.Input<string>;
configSet: pulumi.Input<string>;
solrApiEndpoint: pulumi.Input<string>;
}
class SolrCoreProvider implements pulumi.dynamic.ResourceProvider {
// 检查 Core 是否存在
private async coreExists(name: string, endpoint: string): Promise<boolean> {
try {
const response = await axios.get(`${endpoint}/solr/admin/cores?action=STATUS&core=${name}`);
return response.data.status[name] && Object.keys(response.data.status[name]).length > 0;
} catch (error) {
// Solr 在 core 不存在时可能会返回非 200 状态码
return false;
}
}
// 创建资源
async create(inputs: SolrCoreInputs): Promise<pulumi.dynamic.CreateResult> {
const coreName = inputs.name;
const endpoint = inputs.solrApiEndpoint;
if (await this.coreExists(coreName, endpoint)) {
pulumi.log.info(`Solr core '${coreName}' already exists. Skipping creation.`);
return { id: coreName, outs: inputs };
}
const createUrl = `${endpoint}/solr/admin/cores?action=CREATE&name=${coreName}&instanceDir=${coreName}&configSet=${inputs.configSet}`;
try {
await axios.get(createUrl);
pulumi.log.info(`Successfully created Solr core '${coreName}'.`);
} catch (error: any) {
throw new Error(`Failed to create Solr core '${coreName}': ${error.response?.data?.error?.msg || error.message}`);
}
return { id: coreName, outs: inputs };
}
// 删除资源
async delete(id: string, props: SolrCoreInputs): Promise<void> {
const endpoint = props.solrApiEndpoint;
const unloadUrl = `${endpoint}/solr/admin/cores?action=UNLOAD&core=${id}&deleteInstanceDir=true`;
try {
await axios.get(unloadUrl);
pulumi.log.info(`Successfully deleted Solr core '${id}'.`);
} catch (error: any) {
// 忽略 "core not found" 错误,以保证幂等性
if (error.response?.data?.error?.msg.includes("Cannot find core")) {
pulumi.log.warn(`Solr core '${id}' not found during deletion. Assuming it's already gone.`);
} else {
throw new Error(`Failed to delete Solr core '${id}': ${error.message}`);
}
}
}
}
// 将动态 Provider 封装成一个易于使用的 Component
class SolrCore extends pulumi.dynamic.Resource {
constructor(name: string, args: SolrCoreInputs, opts?: pulumi.CustomResourceOptions) {
super(new SolrCoreProvider(), name, args, opts);
}
}
// =============================================================================
// 3. 声明式地定义应用配置
// =============================================================================
// 为商品搜索服务创建 Solr Core
const productSearchCore = new SolrCore("product-search-core", {
name: "products_v3",
configSet: "_default",
solrApiEndpoint: solrApiUrl,
});
// 使用官方 Kong Provider 创建 Service,指向 Solr Core
const productService = new kong.Service("product-service", {
name: "product-search-service-v3",
protocol: "http",
// `pulumi.interpolate` 用于安全地组合输出值
host: solrApiUrl.apply(url => new URL(url).hostname),
port: solrApiUrl.apply(url => parseInt(new URL(url).port, 10)),
path: pulumi.interpolate`/solr/${productSearchCore.id}`,
});
// 创建 Route
const productRoute = new kong.Route("product-route", {
name: "product-route-v3",
protocols: ["https"],
hosts: ["api.our-company.com"],
paths: ["/search/products/v3"],
serviceId: productService.id,
stripPath: true,
});
// 添加插件,并使用编程逻辑
const productAclPlugin = new kong.Plugin("product-acl", {
name: "acl",
serviceId: productService.id,
configJson: JSON.stringify({
whitelist: ["internal-group", "partner-group"],
// 这里的逻辑在 HCL 中很难实现
hide_groups_header: process.env.NODE_ENV === "production" ? true : false,
}),
});
// 导出关键 URL,方便在 CI/CD 中进行集成测试
export const searchApiEndpoint = pulumi.interpolate`https://${productRoute.hosts[0]}${productRoute.paths[0]}`;
当应用团队运行 pulumi up
时,会得到一个类似 terraform plan
的清晰预览。如果他们从代码中删除了 productSearchCore
的定义,Pulumi 会明确地告诉他们将调用 delete
方法来卸载这个 Core。整个过程是声明式的、可预测的,并且完全与底层设施的变更解耦。
架构的扩展性与局限性
扩展性:
这个分层 IaC 模型非常清晰。平台团队专注于基础设施的可靠性和成本优化。应用团队则获得了他们需要的自治权,可以在自己的领域内快速迭代。当一个新的微服务需要接入 Kong 和 Solr 时,他们只需在自己的 Pulumi 项目中实例化一个新的 Microservice
组件(可以进一步封装上述代码),而无需触碰任何基础设施代码。
局限性:
- 工具链复杂度:引入 Pulumi 意味着团队需要维护和学习另一套工具链。虽然它使用标准编程语言,但其编程模型(如
Input<T>
和Output<T>
)需要时间来适应。 - 状态边界:
StackReference
在两个世界之间建立了联系,但也引入了一种隐式依赖。如果平台团队在 Terraform 中重命名了output
,应用团队的 Pulumi 执行就会失败。这需要通过严格的命名约定和团队间的沟通来缓解。 - 动态 Provider 的维护成本:为 Solr 编写的
SolrCoreProvider
是一个强大的后门,但也意味着我们承担了维护它的责任。当 Solr API 发生变化时,我们需要同步更新这个 Provider 的代码。在真实项目中,这部分代码需要有完善的单元测试和错误处理。
尽管存在这些局限性,但与前两种方案相比,Terraform + Pulumi 的分层模型在职责划分、安全性、开发体验和声明式保证之间取得了最佳的平衡。它承认基础设施和应用配置具有不同的生命周期和所有权,并为它们提供了各自最合适的管理工具。这在实践中,是一种更成熟、更具扩展性的架构选择。