Files
claude-api-proxy/src/deploy.ts
D8D Developer 9e97c52f76
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 2m28s
feat: 应用监听端口改为 80,添加 Spot 实例支持
- 默认端口从 3000 改为 80
- Dockerfile 更新 EXPOSE 和健康检查端口
- 添加 ECI Spot 抢占式实例注解
- 移除 USER node 以支持监听 80 端口

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-03-20 13:12:28 +00:00

379 lines
9.4 KiB
TypeScript

/**
* 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<string, string>;
/** 标签 */
labels?: Record<string, string>;
/** 域名后缀 */
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<DeployProxyResult> {
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;
}
}