解耦基础设施与应用配置:Kong 和 Solr 体系下的 IaC 工具链决策


在一个已经稳定运行的系统中,平台工程团队使用 Terraform 精准地管理着所有底层云资源——从 VPC 网络到 Kubernetes 集群,再到独立的 Solr 云主机集群。一切看起来井然有序,基础设施的变更遵循着严格的 GitOps 流程。然而,混乱潜藏在基础设施与应用配置的边界。应用开发团队需要频繁地变更 Kong API 网关的路由规则、插件配置,以及为新业务创建 Solr Core 并更新 Schema。

当前的流程是,应用团队向平台团队提 PR,修改一个庞大臃肿的 Terraform 项目。这导致了几个核心痛点:

  1. 发布耦合:一个简单的 Kong 路由变更,却要触发整个基础设施的 terraform plan,执行速度缓慢且风险不可控。
  2. 权限失控:为了自治,一些团队要求获得 Terraform 的 apply 权限,这在安全上是完全无法接受的。
  3. 表达力受限: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 文件管理,避免了状态漂移。

劣势与陷阱:
这里的坑远比优势来得深刻。

  1. 状态文件锁定与污染:这是最致命的问题。当应用A的开发人员想修改 kong_route.product_search_route 时,他需要锁定整个 state 文件。如果此时平台团队正在进行网络变更,就会产生冲突。更糟糕的是,应用层的频繁变更会不断地在 state 文件中产生新的版本,将其历史记录与关键的基础设施变更日志混杂在一起。
  2. 爆炸半径不可控terraform apply 的影响范围是整个项目。一个错误的 HCL 变量引用,可能导致应用团队意外地删除了生产环境的数据库实例。在真实项目中,这种风险是不可接受的。
  3. Provider 的局限性:Kong Provider 相当成熟,但对于 Solr 这种 API 复杂的应用,情况就不同了。创建一个 Solr Core 并不仅仅是调用一个 CREATE API endpoint。它可能涉及上传配置文件、设置副本、重载集合等一系列有序操作。用 HCL 来描述这种过程化的逻辑是一种折磨,通常需要借助 null_resourcelocal-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,即使没有现成的,使用 uricommand 模块调用 API 和 CLI 也非常方便。

劣势与陷阱:

  1. 缺乏真正的状态管理:Ansible 本质上是一个配置工具,而非状态管理工具。它不知道资源的“当前状态”。为了实现幂等性,每个 task 都需要开发者手动编写 changed_whenfailed_when 逻辑,这非常容易出错。如果有人手动删除了 Kong 的一个路由,Ansible 在下次运行时不会感知到这个“漂移”,除非你编写了专门的检查任务。
  2. 删除操作的风险:声明式工具如 Terraform 有一个巨大的优势:当你从代码中移除一个资源定义时,plan 会明确告诉你它将被删除。在 Ansible 中,实现“清理”操作需要编写一个完全独立的 Playbook 或 Role,用于删除不再需要的配置。这使得资源的全生命周期管理变得复杂和危险。
  3. 测试困难:测试一个调用远程 API 的 Ansible Playbook 是出了名的困难。你需要依赖 Molecule 这样的工具和复杂的 Mock 环境。相比之下,terraform plan 提供了一个无需真实执行就能预览变更的安全网。

Ansible 方案解决了职责分离的问题,但牺牲了声明式 IaC 的核心优势——状态管理和变更预览。它更像是一组自动化脚本的集合,而不是一个可靠的、描述最终状态的系统。

最终选择:引入 Pulumi 作为应用声明式管理层

我们需要一种工具,它既能像 Terraform 一样进行状态管理和声明式定义,又能像 Ansible 一样提供更强的编程能力和逻辑表达,同时还能与现有的 Terraform 基础设施无缝集成。Pulumi 完美地契合了这个角色。

最终的架构决策如下:

  1. **Terraform (平台层)**:职责不变,管理核心基础设施,并通过 terraform output 暴露出必要的端点信息,例如 Kong Admin API URL 和 Solr Host。
  2. **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 组件(可以进一步封装上述代码),而无需触碰任何基础设施代码。

局限性:

  1. 工具链复杂度:引入 Pulumi 意味着团队需要维护和学习另一套工具链。虽然它使用标准编程语言,但其编程模型(如 Input<T>Output<T>)需要时间来适应。
  2. 状态边界StackReference 在两个世界之间建立了联系,但也引入了一种隐式依赖。如果平台团队在 Terraform 中重命名了 output,应用团队的 Pulumi 执行就会失败。这需要通过严格的命名约定和团队间的沟通来缓解。
  3. 动态 Provider 的维护成本:为 Solr 编写的 SolrCoreProvider 是一个强大的后门,但也意味着我们承担了维护它的责任。当 Solr API 发生变化时,我们需要同步更新这个 Provider 的代码。在真实项目中,这部分代码需要有完善的单元测试和错误处理。

尽管存在这些局限性,但与前两种方案相比,Terraform + Pulumi 的分层模型在职责划分、安全性、开发体验和声明式保证之间取得了最佳的平衡。它承认基础设施和应用配置具有不同的生命周期和所有权,并为它们提供了各自最合适的管理工具。这在实践中,是一种更成熟、更具扩展性的架构选择。


  目录