/** * K8S 部署模块 - 用于部署 Claude API 代理服务到 ECI */ import { config } from 'dotenv'; // 使用 override: true 确保覆盖系统环境变量 config({ override: true }); import { K8sClient, V1Deployment, V1Ingress, V1IngressRule, V1ServicePort } from './k8s-client'; // 环境变量 const K8S_API_URL = process.env.K8S_API_URL || ''; const K8S_API_TOKEN = process.env.K8S_API_TOKEN || ''; const DEV_HOST = process.env.DEV_HOST || 'dev.d8d.fun'; const TLS_SECRET_NAME = process.env.DEV_TLS_SECRET_NAME || 'd8d-proxy-tls'; // 专用命名空间 const NAMESPACE = 'd8d-proxy'; // 初始化 K8S 客户端 export const k8sClient = new K8sClient(K8S_API_URL, K8S_API_TOKEN); export interface DeployProxyParams { /** 容器组名称(唯一标识) */ name: string; /** Docker 镜像 */ image: string; /** 副本数 */ replicas?: number; /** 容器端口 */ port?: number; /** ECI 实例规格 */ instanceType?: string; /** 是否自动创建 EIP */ autoEip?: boolean; /** EIP 带宽 (Mbps) */ eipBandwidth?: number; /** 环境变量 */ env?: Record; /** 标签 */ labels?: Record; /** 域名后缀 */ hostSuffix?: string; } export interface DeployProxyResult { success: boolean; message: string; hosts?: string[]; deploymentName?: string; serviceName?: string; ingressName?: string; } /** * 部署 Claude API 代理服务 */ export async function deployProxy(params: DeployProxyParams): Promise { const { name, image, replicas = 1, port = 3000, instanceType = 'ecs.t5-lc2m1.nano', autoEip = true, eipBandwidth = 5, env = {}, labels = {}, hostSuffix = DEV_HOST, } = params; const deploymentName = name; const serviceName = `service-${name}`; const ingressName = `${name}-ingress`; // 清理域名后缀 const cleanHostSuffix = hostSuffix.replace(/^\*\./, '').replace(/^\.+/, ''); // 构建域名 const host = `${name}-${port}.${cleanHostSuffix}`; try { // 1. 创建 Deployment const deployment: V1Deployment = { metadata: { name: deploymentName, labels: { app: name, 'proxy-container': 'true', ...labels, }, annotations: { // ECI 实例规格 'k8s.aliyun.com/eci-use-specs': instanceType, // 抢占式实例 'k8s.aliyun.com/eci-spot-instance': 'true', // 自动创建 EIP ...(autoEip && { 'k8s.aliyun.com/eci-open-enable-eip': 'true', 'k8s.aliyun.com/eci-eip-bandwidth': String(eipBandwidth), 'k8s.aliyun.com/eci-eip-internet-charge-type': 'PayByBandwidth', }), }, }, spec: { replicas, selector: { matchLabels: { app: name, }, }, template: { metadata: { labels: { app: name, 'proxy-container': 'true', ...labels, }, annotations: { // ECI 实例规格 'k8s.aliyun.com/eci-use-specs': instanceType, // 抢占式实例 'k8s.aliyun.com/eci-spot-instance': 'true', // 自动创建 EIP ...(autoEip && { 'k8s.aliyun.com/eci-open-enable-eip': 'true', 'k8s.aliyun.com/eci-eip-bandwidth': String(eipBandwidth), 'k8s.aliyun.com/eci-eip-internet-charge-type': 'PayByBandwidth', }), }, }, spec: { automountServiceAccountToken: false, enableServiceLinks: false, nodeSelector: { type: 'virtual-kubelet', }, containers: [ { name: name, image: image, ports: [ { containerPort: port, protocol: 'TCP', }, ], env: Object.entries(env).map(([key, value]) => ({ name: key, value, })), resources: { requests: { cpu: '250m', memory: '256Mi', }, }, readinessProbe: { httpGet: { path: '/health', port: port, }, initialDelaySeconds: 5, periodSeconds: 10, }, livenessProbe: { httpGet: { path: '/health', port: port, }, initialDelaySeconds: 15, periodSeconds: 20, }, }, ], tolerations: [ { key: 'alibabacloud.com/eci', operator: 'Exists', effect: 'NoSchedule', }, ], }, }, }, }; await k8sClient.createDeployment({ json: { namespace: NAMESPACE, deployment, }, }); console.log(`✅ Deployment ${deploymentName} 创建成功`); // 2. 创建 Service const ports: V1ServicePort[] = [ { port: port, targetPort: port, name: 'http', }, ]; await k8sClient.createService({ json: { namespace: NAMESPACE, service: { metadata: { name: serviceName, labels: { app: name, }, }, spec: { type: 'ClusterIP', selector: { app: name, }, ports, }, }, }, }); console.log(`✅ Service ${serviceName} 创建成功`); // 3. 创建 Ingress const ingressRule: V1IngressRule = { host, http: { paths: [ { path: '/', pathType: 'Prefix', backend: { service: { name: serviceName, port: { number: port, }, }, }, }, ], }, }; const ingress: V1Ingress = { metadata: { name: ingressName, annotations: { 'nginx.ingress.kubernetes.io/proxy-body-size': '50m', 'nginx.ingress.kubernetes.io/proxy-read-timeout': '300', 'nginx.ingress.kubernetes.io/proxy-send-timeout': '300', }, }, spec: { tls: [ { hosts: [host], secretName: TLS_SECRET_NAME, }, ], rules: [ingressRule], }, }; await k8sClient.createIngress({ json: { namespace: NAMESPACE, ingress, }, }); console.log(`✅ Ingress ${ingressName} 创建成功`); return { success: true, message: `代理服务 ${name} 部署成功`, hosts: [`https://${host}`], deploymentName, serviceName, ingressName, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`❌ 部署失败: ${errorMessage}`); return { success: false, message: `部署失败: ${errorMessage}`, }; } } /** * 删除代理服务 */ export async function deleteProxy(name: string): Promise<{ success: boolean; message: string }> { const ingressName = `${name}-ingress`; const serviceName = `service-${name}`; try { // 1. 删除 Ingress await k8sClient.deleteIngress({ param: { namespace: NAMESPACE, ingressName, }, }).catch(err => console.warn(`删除 Ingress 失败: ${err.message}`)); // 2. 删除 Service await k8sClient.deleteService({ param: { namespace: NAMESPACE, serviceName, }, }).catch(err => console.warn(`删除 Service 失败: ${err.message}`)); // 3. 删除 Deployment await k8sClient.deleteDeployment({ param: { namespace: NAMESPACE, name, }, }).catch(err => console.warn(`删除 Deployment 失败: ${err.message}`)); console.log(`✅ 代理服务 ${name} 删除成功`); return { success: true, message: `代理服务 ${name} 删除成功` }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, message: `删除失败: ${errorMessage}` }; } } /** * 获取代理服务状态 */ export async function getProxyStatus(name: string): Promise<{ exists: boolean; status?: string; hosts?: string[]; }> { try { const deployment = await k8sClient.getDeployment({ param: { namespace: NAMESPACE, name, }, }); const readyReplicas = deployment.status?.readyReplicas || 0; const replicas = deployment.spec?.replicas || 1; // 获取 Ingress 域名 const ingressResult = await k8sClient.getIngress({ param: { namespace: NAMESPACE, ingressName: `${name}-ingress`, }, }); const hosts = ingressResult.ingress?.spec?.rules?.map(r => `https://${r.host}`) || []; return { exists: true, status: readyReplicas >= replicas ? 'Running' : 'Pending', hosts, }; } catch (error) { const errorCode = (error as { code?: number })?.code; if (errorCode === 404) { return { exists: false }; } throw error; } }